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.
53 Comments
How did you complete that loginAction and bootrow?
Hey Richard ,
What do you mean? You may find all the code in this screencast below the video in course script. Our code blocks are expandable, it means you can click on double arrow against the line to load more context of the file. Or you can download the whole course project code in up right corner: "Download" -> "Course Code" if you want to see the final code to the end of this course - but it's available for users who one the course or have paid subscription only. Does it makes sense for you?
Cheers!
No. As you typed the bootstrap code it auto completed. Some sort of downloadable template for Idea editor?
Hey Richard
I believe you are talking about "live templates", if you are using PHPStorm, you can create your own templates by going to: settings > editor > live templates
You can read more info here: https://www.jetbrains.com/h...
Have a nice day
Oh, I thought KNP had a library of them somewhere. I shall google more. Thank you.
Hey Richard ,
Yea, we have one, check it out here: https://github.com/knpunive... . But as Diego said, you can create your owns very easy. Check out our screencast about PhpStorm to get more info about how to do it: https://knpuniversity.com/s...
Cheers!
One question guys do we need to put to the login form some csrf protection?
Hey Ponrad,
Good question! Yes, it's a good practice. And Symfony docs recommend to do this to protect your login form against CSRF attacks: http://symfony.com/doc/curr...
Cheers!
Error " Variable "form" does not exist in security\login.html.twig at line 11"
Controller Security
namespace Admin\AdminBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Admin\AdminBundle\Form\LoginForm;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class SecurityController extends Controller
{
/**
* @Route("/login", name="security_login")
*/
public function loginAction()
{
$authenticationUtils = $this->get('security.authentication_utils');
$error = $authenticationUtils->getLastAuthenticationError();
// Ия пользователя
$lastUsername = $authenticationUtils->getLastUsername();
$form = $this->createForm(LoginForm::class, [
'_username' => $lastUsername,
]);
return $this->render(
'security/login.html.twig',
array(
'last_username' => $lastUsername,
'error' => $error,
)
);
}
}
LoginForm Class
namespace Admin\AdminBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
class LoginForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('_username')
->add('_password', PasswordType::class);
}
}
login.html.twig
{% extends 'base.html.twig' %}
{% block body %}
<div class="row">
<div class="small-12 medium-12 large-12 columns">
<h2>Login</h2>
{% if error %}
<div>{{ error.messageKey|trans(error.messageDate, 'security') }}</div>
{% endif %}
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_row(form._username) }}
{{ form_row(form._password) }}
<button type="submit">login</button>
{{ form_end(form) }}
</div>
</div>
{% endblock %}
Hey Andrey,
What's your question here? :)
I think you probably forget to pass $form to the template, here's a fix:
P.S. Don't forget to call "createView()" method on form object before passing it to the template.
Cheers!
AppBundle\Controller\:
resource: '../../src/AppBundle/Controller'
public: true
tags: ['controller.service_arguments']
All things are same as u mentioned but still i am getting error following error
Controller
"EmployeeBundle\Controller\webLoginController::loginAction()" requires
that you provide a value for the "$authUtils" argument. Either the
argument is nullable and no null value has been provided, no default
value has been provided or because there is a non optional argument
after this one.
Iam using symfony 3.3.5V
Hey Asimsagir Abbasi
Could you show me your controller's code? So, I can help you debugging.
Cheers!
Hi,thanks for your reply I have solved my issue:)
Cool! I'm glad to hear you could fix it by yourself :)
:) but i am facing new prblm ,i am always receiving invalid credentials . i am using my own EmployeeBundle
pls let me know why this happening
following is my security.yml file codes:
This is my controller:
And this is my login form:
Hey Asimsagir,
Input fields names are important here, they should be "_username" and "_password". Please, notice the letter case for username: "_username" not "_userName", etc.. I think that's the problem.
Cheers!
yes I think u r right that's the issue .. thnks
This no longer works with the demo code from the symfony site, you need to copy it from the script or the downloads finish folder.
Yo Robert Went!
You're right that you can always copy from the finish folder :). But, I *am* curious which part changed - was it the login template? Or the login controller? I *think* I know the issue (and it's my fault - I updated the documentation on Symfony.com): In Symfony 3.3, you can fetch services in a new way (we're talking about it here: http://knpuniversity.com/sc.... The Symfony.com documentation has been updated to use this... which is cool! Except if you're using a Symfony 3.2 project... and then you try to copy from the 3.3 version of the docs (which show up by default). If I'm right, then it was the controller code that was the problem. Was that it?
Oh, also, for other people, we do have the code on each page below the video - so if you ever want to copy a bit of code, you can grab it there really quickly.
Cheers!
Ah yeah, that makes sense.
It was the controller code for the login action from the 3.3 version which shows up as default
Ah, I was actually expending an error with the
$autUtilspart! :) As long as you have theusestatement for theRequest, that should actually pass you the request (in any Symfony version). But anyways... there is a legit difference in 3.3 - so I'm glad you asked!Cheers!
Its not the request part :
public function loginAction(Request $request, AuthenticationUtils $authUtils)
Its the authenticationUtils parameter. That doesnt work. Interestingly the symfony docs go from:
public function loginAction(Request $request)
to
public function loginAction(Request $request, AuthenticationUtils $authUtils)
without any explanation. Is this some sort for dependency injection perhaps or "auto something or other" that saves us having to query the container?
Hey Max,
Yes, this auto-injection feature by typehint in actions allows do not use container directly, i.e. fetch services from the container, and as a consequence, you can make more your services private, what is cool.
Cheers!
Hey Ryan,
Actually I got an error with the updated code from Symfony doc; I'm running 3.3.2 so I thought I'd try it :)
Controller "AdminBundle\Controller\SecurityController::loginAction()" requires that you provide a value for the "$authUtils" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one.
There isn't much on the official doc to help me figure it out; I know I'm supposed to pass the $authUtils argument but what is it exactly? :p
Hey Edouard PERRIN!
Hmm, let's see if we can find your problem... and maybe also improve the docs at the same time :).
Do you have the
usestatement for theAuthenticationUtilsclass? It would be:If you do have that, can you post your
loginActionexactly? Also, is this a new project or did you upgrade from an older version of Symfony? What does your services.yml file look like?Cheers!
Hey weaverryan,
Ok, so I thought it was working but trying the new code again, it still doesn't work.
Everything works with the old way btw.
I have the use statement; I upgraded mid-project from Symfony 3.something to 3.3.2
Services.yml
https://pastebin.com/RsTi71bZ
Security.yml
https://pastebin.com/ztPu8ZPR
SecurityController
https://pastebin.com/340jchpb
LoginForm
https://pastebin.com/3tBc5Wy0
LoginFormAuthenticator
https://pastebin.com/nSMfwJR6
Yo Edouard PERRIN!
Ah, ok, I've got it! In order for the "service controller argument" thing to work (http://knpuniversity.com/sc..., your controller must be registered as a service. Basically, when you upgrade from Symfony 3.2 to 3.3, *if* you want to use all of the new features, you need some config changes in services.yml. We talk about this in the 3.3 tutorial: http://knpuniversity.com/sc.... Most notably, you need this bit of configuration: http://knpuniversity.com/sc.... In fact, I think you could add just those lines (I'm talking about the first code block on that page - lines 23-28) and that would do it :).
Cheers!
Nice catch weaverryan!! That was the right move, it works smoothly now. Guess I better take a few moments to watch the 3.3 tutorial :)
Thanks for your help!
Woohoo! Cheers! It's a tricky transition (but ultimately optional) to the new fancy stuff ;)
I'm getting the same issue, using 3.3.0.
Even though I do have the controllers added in the services.yml.
And I have the use statement:
However, when I was using basically the same code in a project that was 3.3.4 (i think?) it worked without a hiccup. I'm going to walk away and come back, because it seems like something I'm missing...unless the minor version makes a difference here.
That said, it does work if I just use:
Hey Geoff,
Yep, it should work for public services well, but if you have private one but can't get that controller's argument injection worked - you need to manually make your services public. Thanks for sharing this workaround for others!
Cheers!
Hey Geoff.
Hm, it should work for 3.3.0. I'm not 100% sure but probably you need to add:
I think it should help, just don't forget to clear the cache after this change. If it does not help, could you make sure that your Symfony dependencies are locked at least at those versions: https://github.com/symfony/symfony-standard/blob/3.3/composer.json#L16-L28 - I suppose it's not enough to just have symfony/symfony 3.3.0.
Cheers!
Yeah, not sure what's up. As per my last comment, i tried 3.3.4 with this project and it made no difference. So it must be another config issue. I do have that already set in services.yml
An additional followup here...I tried to update the project to 3.3.4, and it's still throwing the same error. So it's not the symfony version. Not sure what else to try to get this to work injecting the AuthenticationUtils class.
The weird thing is in my OTHER project that is on 3.3.4 and has basically the same code, the services.yml files are almost identical (the only differences are unrelated services).
Hey geoff,
I see the latest version of Symfony is v3.3.11 in that branch. Why didn't you update the project to the latest one? And btw, how did you update it? what steps did you use? Tweaked "symfony/symfony" version to "3.3.4" in composer.json and ran exactly "composer update" command?
Cheers!
It's possible some context of my issue got lost here because it's in different comments - I updated to 3.3.4 specifically because injecting AuthenticationUtils is working in another very similar project I have (it was a test project I started with most of the same code). However, when I updated in this project, it made no difference. I could try 3.3.11, but I don't think that's the issue.
More context: Another similar project that I have that is on 3.3.4 - so that's why I tried it.
OK, great. But what about your way of updating dependencies? Did you tweak your dependencies versions to match these minor versions: https://github.com/symfony/... ? Also, you need to run "composer update" to upgrade all the packages, not only "symfony/symfony" one.
Cheers!
As a test, I injected the class into the constructor rather than directly into the loginAction, and that worked. Any thought on why that would be the case?
Hm, ok, then 2 questions:
Probably somehow it does not match criteria of this filter:
and registers as a service.
Cheers!
Thanks for following up on this. Unfortunately I don't really see an issue there.
services.yml
Path: project\app\config\services.yml
SecurityController.php
Path: project\src\AppBundle\Controller\SecurityController.php
Hey Geoff Maddock
Can you try something for me? Create a new action method in that controller and inject any other service, let's see if that works
I had the same issue.
public function loginAction() {
// instead of using dependency injection fill the variable straight with get() method.
$authUtils = $this->get('security.authentication_utils');
// get the login error if there is one
$error = ........
+1 this code (as we do it in the tutorial) is safe in all versions of Symfony :). There is a different way to do it also in Symfony 3.3 - but if you're not sure, just use this way - it's totally great!
Thanks for the comment!
welcome ;)
SF 3.3.6 doesn't have getName() after form creation but it does have getBlockPrefix(). Should this function be removed as well?
Hey Mike P.
Yeah, feel free to remove it from your Form class, it is used to name the Form element and the default value is really good.
From docs: The block prefix defaults to the underscored short class name with the "Type" suffix removed (e.g. "UserProfileType" => "user_profile").
Cheers!
there hi,
Yip it's1 me again, Got a question about phpStorm.
So 1. the ACTION Live Template I got and created but..
2. The "options -> Create template" shortcut key ??
First I'm on windows 10 pro, yip I know bad misstake I know but Mac book pro is just out of budget reach.. you get.
I did do one thing correct I bought phpStorm v2017.3.4
Am I missing a plugin ?
OK found it ... on windows "Alt+Enter" pfff
"Houston: no signs of life"
Start the conversation!