Rendering that Login Form

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Time to build a login form. And guess what? This page is no different than every other page: we'll create a route, a controller and render a template.

For organization, create a new class called SecurityController. Extend the normal Symfony base Controller and add a public function loginAction():

... lines 1 - 2
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
... lines 6 - 7
class SecurityController extends Controller
{
... lines 10 - 12
public function loginAction()
{
}
}

Setup the URL to be /login and call the route security_login:

... lines 1 - 5
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class SecurityController extends Controller
{
/**
* @Route("/login", name="security_login")
*/
public function loginAction()
{
}
}

Make sure to auto-complete the @Route annotation so you get the use statement up top.

Cool!

Every login form looks about the same, so let's go steal some code. Google for "Symfony security form login" and Find a page called How to Build a Traditional Login Form.

Adding the Login Controller

Find their loginAction(), copy its code and paste it into ours:

... lines 1 - 7
class SecurityController extends Controller
{
... lines 10 - 12
public function loginAction()
{
$authenticationUtils = $this->get('security.authentication_utils');
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render(
'security/login.html.twig',
array(
// last username entered by the user
'last_username' => $lastUsername,
'error' => $error,
)
);
}
}

Notice, one thing is immediately weird: there's no form processing code inside of here. Welcome to the strangest part of Symfony's security. We will build the login form here, but some other magic layer will actually handle the form submit. We'll build that layer next.

But thanks to this handy security.authentication_utils service, we can at least grab any authentication error that may have just happened in that magic layer as well as the last username that was typed in, which will actually be an email address for us.

The Login Controller

To create the template, hit Option+enter on a Mac and select the option to create the template. Or you can go create this by hand.

You guys know what to do: add {% extends 'base.html.twig' %}. Then, override {% block body %} and add {% endblock %}. I'll setup some markup to get us started:

{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Login!</h1>
... lines 8 - 27
</div>
</div>
</div>
{% endblock %}

Great! This template also has a bunch of boilerplate code, so copy that from the docs too. Paste it here. Update the form action route to security_login:

... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Login!</h1>
{% if error %}
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
<form action="{{ path('security_login') }}" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
{#
If you want to control the URL the user
is redirected to on success (more details below)
<input type="hidden" name="_target_path" value="/account" />
#}
<button type="submit">login</button>
</form>
</div>
</div>
</div>
{% endblock %}

Well, it ain't fancy, but let's try it out: go to /login. There it is, in all its ugly glory.

What, No Form Class?

Now, I bet you've noticed something else weird: we are not using the form system: we're building the HTML form by hand. And this is totally ok. Security is strange because we will not handle the form submit in the normal way. Because of that, most people simply build the form by hand: you can do it either way.

But... our form is ugly. And I know from our forms course, that the form system is already setup to render using Bootstrap-friendly markup. So if we did use a real form... this would instantly be less ugly.

Ok, Ok: Let's add a Form Class

So let's do that: in the Form directory, create a new form class called LoginForm. Remove getName() - that's not needed in Symfony 3 - and configureOptions():

... lines 1 - 2
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
... line 6
use Symfony\Component\Form\FormBuilderInterface;
... lines 8 - 9
class LoginForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 14 - 17
}
}

This is a rare time when I won't bother binding my form to a class.

Tip

If you're building a login form that will be used with Symfony's native form_login system, override getBlockPrefix() and make it return an empty string. This will put the POST data in the proper place so the form_login system can find it.

In buildForm(), let's add two things, _username and _password, which should be a PasswordType:

... lines 1 - 5
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
... lines 7 - 9
class LoginForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('_username')
->add('_password', PasswordType::class)
;
}
}

You can name these fields anything, but _username and _password are common in the Symfony world. Again, we're calling this _username, but for us, it's an email.

Next, open SecurityController and add $form = $this->createForm(LoginForm::class):

... lines 1 - 6
use AppBundle\Form\LoginForm;
class SecurityController extends Controller
{
... lines 11 - 13
public function loginAction()
{
... lines 16 - 23
$form = $this->createForm(LoginForm::class, [
... line 25
]);
... lines 27 - 34
}
}

And, if the user just failed login, we need to pre-populate their _username field. To pass the form default data, add a second argument: an array with _username set to $lastUsername:

... lines 1 - 23
$form = $this->createForm(LoginForm::class, [
'_username' => $lastUsername,
]);
... lines 27 - 36

Finally, skip the form processing: that will live somewhere else. Pass the form into the template, replacing $lastUsername with 'form' => $form->createView():

... lines 1 - 8
class SecurityController extends Controller
{
... lines 11 - 13
public function loginAction()
{
... lines 16 - 27
return $this->render(
'security/login.html.twig',
array(
'form' => $form->createView(),
'error' => $error,
)
);
}
}

Rendering the Form in the Template

Open up the template, Before we get to rendering, make sure our eventual error message looks nice. Add alert alert-danger:

... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Login!</h1>
{% if error %}
<div class="alert alert-danger">
{{ error.messageKey|trans(error.messageData, 'security') }}
</div>
{% endif %}
... lines 14 - 19
</div>
</div>
</div>
{% endblock %}

Now, kill the entire form and replace it with our normal form stuff: form_start(form), from_end(form), form_row(form._username) and form_row(form._password):

... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Login!</h1>
{% if error %}
<div class="alert alert-danger">
{{ error.messageKey|trans(error.messageData, 'security') }}
</div>
{% endif %}
{{ form_start(form) }}
{{ form_row(form._username) }}
{{ form_row(form._password) }}
... line 18
{{ form_end(form) }}
</div>
</div>
</div>
{% endblock %}

Don't forget your button! type="submit", add a few classes, say Login and get fancy with a lock icon:

... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
... lines 7 - 14
{{ form_start(form) }}
... lines 16 - 17
<button type="submit" class="btn btn-success">Login <span class="fa fa-lock"></span></button>
{{ form_end(form) }}
</div>
</div>
</div>
{% endblock %}

We did this purely so that Ryan could get his form looking less ugly. Let's see if it worked. So much better!

Oh, while we're here, let's hook up the Login button on the upper right. This lives in base.html.twig. The login form is just a normal route, so add path('security_login'):

<!DOCTYPE html>
<html>
... lines 3 - 13
<body>
... lines 15 - 19
<header class="header">
... lines 21 - 22
<ul class="navi">
... line 24
<li><a href="{{ path('security_login') }}">Login</a></li>
</ul>
</header>
... lines 28 - 46
</body>
</html>

Refresh, click that link, and here we are.

Login form complete. It's finally time for the meat of authentication: it's time to build an authenticator.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}