Building a 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.

There are a lot of ways that you can allow your users to log in... one way being a login form that loads users from the database. That's what we're going to build first.

The easiest way to build a login form system is by running a symfony console make:auth command. That will generate everything you need. But since we want to really learn security, let's do this step-by-step... mostly by hand.

Before we start thinking about authenticating the user, we first need to build a login page, which... if you think about it... has nothing to do with security! It's just a normal Symfony route, controller & template that renders a form. Let's cheat a little to make this. Run:

symfony console make:controller

Answer SecurityController. Cool! Go open up the new class: src/Controller/SecurityController.php:

... lines 1 - 2
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SecurityController extends AbstractController
{
/**
* @Route("/security", name="security")
*/
public function index(): Response
{
return $this->render('security/index.html.twig', [
'controller_name' => 'SecurityController',
]);
}
}

Nothing too fancy here. Let's customize this to be a login page: set the URL to /login, call the route app_login and rename the method to login():

... lines 1 - 8
class SecurityController extends AbstractController
{
/**
* @Route("/login", name="app_login")
*/
public function login(): Response
{
... line 16
}
}

For the template, call it security/login.html.twig... and don't pass any variables right now:

... lines 1 - 8
class SecurityController extends AbstractController
{
/**
* @Route("/login", name="app_login")
*/
public function login(): Response
{
return $this->render('security/login.html.twig');
}
}

Down in the templates/ directory, open templates/security/... and rename the template to login.html.twig:

{% extends 'base.html.twig' %}
{% block title %}Hello SecurityController!{% endblock %}
{% block body %}
<style>
.example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
.example-wrapper code { background: #F5F5F5; padding: 2px 6px; }
</style>
<div class="example-wrapper">
<h1>Hello {{ controller_name }}! ✅</h1>
This friendly message is coming from:
<ul>
<li>Your controller at <code><a href="{{ '/Users/weaverryan/Sites/knp/knpu-repos/symfony5/src/Controller/SecurityController.php'|file_link(0) }}">src/Controller/SecurityController.php</a></code></li>
<li>Your template at <code><a href="{{ '/Users/weaverryan/Sites/knp/knpu-repos/symfony5/templates/security/index.html.twig'|file_link(0) }}">templates/security/index.html.twig</a></code></li>
</ul>
</div>
{% endblock %}

To get started, I'm going to completely replace this template and paste in a new structure: you can copy this from the code block on this page:

{% extends 'base.html.twig' %}
{% block title %}Log In!{% endblock %}
{% block body %}
<div class="container">
<div class="row">
<div class="login-form bg-light mt-4 p-4">
<form method="post" class="row g-3">
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<div class="col-12">
<label for="inputEmail">Email</label>
<input type="email" name="email" id="inputEmail" class="form-control" required autofocus>
</div>
<div class="col-12">
<label for="inputPassword">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" required>
</div>
<div class="col-12">
<button class="btn btn-lg btn-primary float-end" type="submit">
Sign in
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}

There's nothing fancy here: we extend base.html.twig, override the title block... then we have a form that submits a POST right back to /login. It doesn't have an action attribute, which means it submits back to this same URL. The form has two fields - input name="email" and input name="password" - and a submit button... all with Bootstrap 5 classes to look nice.

Let's add a link to this page from base.html.twig. Search for sign up. Cool. Right before this, add a link with {{ path('app_login') }}, say "Log In"... and give this some classes to make it look nice:

<!DOCTYPE html>
<html>
... lines 3 - 14
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1">
<div class="container-fluid">
... lines 18 - 26
<div class="collapse navbar-collapse" id="navbar-collapsable">
... lines 28 - 33
<a class="nav-link text-black-50" href="{{ path('app_login') }}">Log In</a>
<a href="#" class="btn btn-dark">Sign up</a>
</div>
</div>
</nav>
... lines 39 - 43
</body>
</html>

Let's check it out! Refresh the home page... and click the link. Hello log in page!

And of course, if we fill out the form and submit... absolutely nothing happens! That makes sense. This submits right back to /login... but because we don't have any form-processing logic yet... the page just re-renders.

So next: let's write that processing code. But... surprise! It won't live in the controller. It's time to create an authenticator and learn all about Symfony firewalls.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.4.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.0
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}