Buy

There are two steps to building a login form: the visual part - the HTML form itself - and the logic when you submit that form: finding the user, checking the password, and logging in. The interesting part is... if you think about it, the first part - the HTML form - has absolutely nothing to do with security. It's just... well... a boring, normal HTML form!

Let's get that built first. By the way, there are plans to add a make command to generate a login form and the security logic automatically, so that we only need to fill in a few details. That doesn't exist yet, so.. we'll do it manually. But, that's a bit better for learning anyways.

Creating the Login Controller & Template

To build the controller, let's at least use one shortcut. At your terminal, run:

php bin/console make:controller

to create a new class called SecurityController. Move over and open that:

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

Ok: update the URL to /login, change the route name to app_login and the method to login():

... lines 1 - 7
class SecurityController extends AbstractController
{
/**
* @Route("/login", name="app_login")
*/
public function login()
{
... lines 15 - 17
}
}

We don't need to pass any variables yet, and we'll call the template login.html.twig:

... lines 1 - 7
class SecurityController extends AbstractController
{
... lines 10 - 12
public function login()
{
return $this->render('security/login.html.twig', [
]);
}
}

Next, down in templates/security, rename index.html.twig to login.html.twig. Let's try it! Move over, go to /login and... whoops!

Variable controller_name does not exist.

Duh! I removed the variables that we were passing into the template:

... lines 1 - 7
class SecurityController extends AbstractController
{
... lines 10 - 12
public function login()
{
return $this->render('security/login.html.twig', [
]);
}
}

Empty all of the existing code from the template. Then, change the title to Login! and, for now, just add an h1 with "Login to the SpaceBar!":

{% extends 'base.html.twig' %}
{% block title %}Login!{% endblock %}
{% block body %}
<h1>Login to the SpaceBar!</h1>
{% endblock %}

Filling in the Security Logic & Login Form

Try it again: perfect! Well, not perfect - it looks terrible... and there's no login form yet. To fix that part, Google for "Symfony login form" to find a page on the Symfony docs that talks all about this. We're coming here so that we can steal some code!

Scroll down a bit until you see a login() method that has some logic in it. Copy the body, move back to our controller, and paste!

... lines 1 - 8
class SecurityController extends AbstractController
{
... lines 11 - 13
public function login(AuthenticationUtils $authenticationUtils)
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
... lines 21 - 25
}
}

This needs an AuthenticationUtils class as an argument. Add it: AuthenticationUtils $authenticationUtils:

... lines 1 - 6
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends AbstractController
{
... lines 11 - 13
public function login(AuthenticationUtils $authenticationUtils)
{
... lines 16 - 25
}
}

Then, these two new variables are passed into Twig. Copy them, and also paste it:

... lines 1 - 8
class SecurityController extends AbstractController
{
... lines 11 - 13
public function login(AuthenticationUtils $authenticationUtils)
{
... lines 16 - 21
return $this->render('security/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
}

In a few minutes, we're going to talk about where these two variables are set. They both deal with authentication.

But first, go back to the docs and find the login form. Copy this, move over and paste it into our body:

... lines 1 - 4
{% block body %}
<h1>Login to the SpaceBar!</h1>
{% if error %}
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
<form action="{{ path('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>
{% endblock %}

Notice: there is nothing special about this form: it has a username field, a password field and a submit button. And, we're going to customize it, so don't look too closely yet.

Move back to your browser to check things out. Bah!

Unable to generate a URL for the named route "login"

This comes from login.html.twig. Of course! The template we copied is pointing to a route called login, but our route is called app_login:

... lines 1 - 8
class SecurityController extends AbstractController
{
/**
* @Route("/login", name="app_login")
*/
public function login(AuthenticationUtils $authenticationUtils)
{
... lines 16 - 25
}
}

Actually, just remove the action= entirely:

... lines 1 - 4
{% block body %}
... lines 6 - 11
<form method="post">
... lines 13 - 25
</form>
{% endblock %}

If a form doesn't have an action attribute, it will submit right back to the same URL - /login - which is what I want anyways.

Refresh again. Perfect! Well, it still looks awful. Oof. To fix that, I'm going to replace the HTML form with some markup that looks nice in Bootstrap 4 - you can copy this from the code block on this page:

... lines 1 - 10
{% block body %}
<form class="form-signin" method="post">
{% if error %}
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<label for="inputEmail" class="sr-only">Email address</label>
<input type="email" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"> Remember me
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">
Sign in
</button>
</form>
{% endblock %}

Including the login.css File

Before we look at this new code, try it! Refresh! Still ugly! Dang! Oh yea, that's because we need to include a new CSS file for this markup.

If you downloaded the course code, you should have a tutorial/ directory with two CSS files inside. Copy login.css, find your public/ directory and paste the file into public/css:

body {
background-color: #fff;
}
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: auto;
margin-top: 50px;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-control {
position: relative;
box-sizing: border-box;
height: auto;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}

So far in this series, we are not using Webpack Encore, which is an awesome tool for professionally combining and loading CSS and JS files. Instead, we're just putting CSS files into the public/ directory and pointing to them directly. If you want to learn more about Encore, go check out our Webpack Encore tutorial.

Anyways, we need to add a link tag for this new CSS file... but I only want to include it on this page, not on every page - we just don't need the CSS on every page. Look at base.html.twig:

<!doctype html>
<html lang="en">
<head>
... lines 5 - 8
{% block stylesheets %}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link rel="stylesheet" href="{{ asset('css/font-awesome.css') }}">
<link rel="stylesheet" href="{{ asset('css/styles.css') }}">
{% endblock %}
</head>
... lines 15 - 66
</body>
</html>

We're including three CSS files in the base layout. Ah, and they all live inside a block called stylesheets.

We basically want to add a fourth link tag right below these... but only on the login page. To do that, in login.html.twig, add block stylesheets and endblock:

... lines 1 - 4
{% block stylesheets %}
... lines 6 - 8
{% endblock %}
... lines 10 - 32

This will override that block completely... which is actually not exactly what we want. Nope, we want to add to that block. To do that print parent():

... lines 1 - 4
{% block stylesheets %}
{{ parent() }}
... lines 7 - 8
{% endblock %}
... lines 10 - 32

This will print the content of the parent block - the 3 link tags - and then we can add the new link tag below: link, with href= and login.css. PhpStorm helps fill in the asset() function:

... lines 1 - 4
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/login.css') }}">
{% endblock %}
... lines 10 - 32

Now it should look good. Try it. Boom! Oh, but we don't need that h1 tag anymore.

The Fields of the Login Form

So even though this looks much better, it's still just a very boring HTML form. It has an email field and a password field... though, we won't add the password-checking logic until later. It also has a "remember me" checkbox that we'll learn how to activate.

The point is: you can make your login form look however you want. The only special part is this error variable, which, when we're done, will be the authentication error if the user just entered a bad email or password:

... lines 1 - 10
{% block body %}
<form class="form-signin" method="post">
{% if error %}
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
... lines 16 - 29
</form>
{% endblock %}

I'll plan ahead and add a Bootstrap class for this:

... lines 1 - 10
{% block body %}
<form class="form-signin" method="post">
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
... lines 16 - 29
</form>
{% endblock %}

Ok. Login form is done! But... we probably need a link to this page. In the upper right corner, we have a cute user dropdown... which is totally hardcoded with fake data. Go back to base.html.twig and scroll down to find this. There it is! For now, let's comment-out that drop-down:

<!doctype html>
<html lang="en">
... lines 3 - 15
<body>
<nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5">
... lines 18 - 21
<div class="collapse navbar-collapse" id="navbarNavDropdown">
... lines 23 - 34
<ul class="navbar-nav ml-auto">
... lines 36 - 38
{#
<li class="nav-item dropdown" style="margin-right: 75px;">
<a class="nav-link dropdown-toggle" href="http://example.com" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="nav-profile-img rounded-circle" src="{{ asset('images/astronaut-profile.png') }}">
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="#">Profile</a>
<a class="dropdown-item" href="#">Create Post</a>
<a class="dropdown-item" href="#">Logout</a>
</div>
</li>
#}
</ul>
</div>
</nav>
... lines 54 - 71
</body>
</html>

We'll re-add it later when we have real data. Then, copy a link from above, paste ithere and change it to Login with a link to app_login:

<!doctype html>
<html lang="en">
... lines 3 - 15
<body>
<nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5">
... lines 18 - 21
<div class="collapse navbar-collapse" id="navbarNavDropdown">
... lines 23 - 34
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a style="color: #fff;" class="nav-link" href="{{ path('app_login') }}">Login</a>
</li>
{#
<li class="nav-item dropdown" style="margin-right: 75px;">
<a class="nav-link dropdown-toggle" href="http://example.com" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="nav-profile-img rounded-circle" src="{{ asset('images/astronaut-profile.png') }}">
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="#">Profile</a>
<a class="dropdown-item" href="#">Create Post</a>
<a class="dropdown-item" href="#">Logout</a>
</div>
</li>
#}
</ul>
</div>
</nav>
... lines 54 - 71
</body>
</html>

Try it - refresh! We got it! HTML login form, check! We are now ready to fill in the logic of what happens when we submit the form. We'll do that in something called an "authenticator".

Leave a comment!