Rendering that Login Form
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')
:
<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.
How did you complete that loginAction and bootrow?