Convert Symfony Auth Scaffolding Login to use Form Builder

If you follow various tutorials (like this one) to set up some basic user authentication steps like a login form, logout route, user registration form… you’ll use commands like php bin/console make:registration and php bin/console make:registration which will generate login and registration forms for you. When I did this in Symfony 6, I ended up with a registration form created with Symfony’s Form Builder method and a login form created with a typical html form in the twig templates. Later when adding the symfonycasts/reset-password-bundle flow to add a ‘forgot my password’ step, this created another form using Form Builder. Eventually I ended up with all of my auth steps using Form Builder except the login form. Inconsistency- yuck!

I’m using Symfony 6.2 as of this writing.

Here’s how to switch the login form so it works like the rest.

Considerations

Since you likely have an authenticator set up in your security.yaml file, this is where the submission of the login form and the handling of user authentication happens. This means your form needs to have very specific names on the input fields so that the authenticator can find the values in the POST request. When we define our form type class, we’ll take this into consideration.

If you’ve added a remember me checkbox then we’ll also have to work that in.

Login Form Type

Define your form with the username (in my case that’s the email field on the User entity) and password.

<?php
// src/Form/LoginFormType.php
namespace App\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class LoginFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('email', EmailType::class, [
                'mapped' => false,
                'attr' => ['autocomplete' => 'email'],                
            ])
            ->add('password', PasswordType::class, [
                'mapped' => false,
                'attr' => ['autocomplete' => 'current-password'],                
            ])
            // See https://symfony.com/doc/current/security/remember_me.html
            ->add('_remember_me', CheckboxType::class, [
                'mapped' => false,
                'required' => false,
                'label' => 'Remember Me'
            ])
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'csrf_token_id' => 'authenticate',
        ]);
    }
}

Update The Login Form Template

Update your twig template to present the form like so:

{{ form_start(loginForm) }}

        {{ form_row(loginForm.email, {
                full_name:'email',
                attr: {class: 'form-control'}
            }) }}

        {{ form_row(loginForm.password, {
            full_name:'password',
            attr: {class: 'form-control'}
        }) }}

            {{ form_widget(loginForm._remember_me, {
                full_name:'_remember_me',
            }) }}
            {{ form_label(loginForm._remember_me) }}


        <button type="submit">
            Sign in
        </button>

        <a href="{{ path('app_forgot_password_request') }}">Forgot My Password</a>

        {{ form_row(loginForm._token, {
            full_name:'_csrf_token',
        }) }}

{{ form_end(loginForm) }}

Here we are customizing the layout of the Remember Me checkbox to put the label after the checkbox and we’re adding a link to the Forgot Password step.

Update Login Controller Method

Now your method in the LoginController needs to be updated to setup the new form.

#[Route('/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
        if ($this->getUser()) {
            // User is already logged in.
            return $this->redirectToRoute('profile');
        }

        $form = $this->createForm(LoginFormType::class);

        // Handling of the form submission and user authentication is handled
        // automatically by: custom_authenticator defined in security.yaml.

        // Get the login error if there is one
        $error = $authenticationUtils->getLastAuthenticationError();        

        // last username entered by the user to repopulate the form.
        $lastUsername = $authenticationUtils->getLastUsername();

        $form->get('email')->setData(
            $lastUsername
        );

        return $this->render('authentication/login.twig', [
            'loginForm' => $form->createView(),
            'error' => $error
        ]);
}

Gotchas

A few small headaches I ran into…

  • I had to add a new RememberMeBadge() to the array of Badges for the Passport initialization in src/Security/UserAuthenticator.php for the Remember Me cookie to be set.
  • I had too much time wasted tying to figure out why my form wasn’t submitting to the controller method before I realized that the authenticator captures the login form submission so your controller method won’t have to perform a $form->handleRequest($request) like the others.
  • We don’t need any constraints in the Form Type definition since the form is not processed via $form->handleRequest($request) and the constraints won’t be checked this way. If you’re planning to use this form and handle it the usual way, you’ll want to add constraints like NotBlank on the email and password fields.

Leave a Reply

Your email address will not be published. Required fields are marked *