Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The 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 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!

27
Login or Register to join the conversation
Metin Ö. Avatar
Metin Ö. Avatar Metin Ö. | posted 1 year ago

Good morning or evening! I wonder if there is any chance to make a redirect to a specific page after a logout?
I tried this
$this->redirect($this->generateUrl('security_logout', [
'_target_path' => $this->generateUrl('security_register')
]))

as well as $this->redirectToRoute(...) but that doesnt work. I use the configured logout flow through security.yaml

I know it's very edge-casy, but I want to log users out and redirect them if they are logged in while clicking on the "register" route. And while I'm writing this, I'm wondering how to log out procedurally without redirects... for example in a controller.

Reply

Hey metin

I guess the easiest way is to use "LogoutEvent" or "logout success handler" depending on symfony version. You can read more here https://symfony.com/doc/cur...

Cheers!

Reply
Metin Ö. Avatar

So it'll be a back and forth flow

Reply
Brent Avatar

Hi, I created a login form almost exactly as in this tutorial. Works great. Now I need to have a separate LoginFormAuthenticator that grabs the user input and validates against an Ldap server, sort of an employee login with the goal being to skip the whole registration process allowing employees to use their active directory creds to authenticate. I installed the Ldap package, added a service, a separate user provider, firewall, routes and login form. Also, this application uses EasyAdmin, fyi. Is this possible to have two separate logins for authentication processed in the same application? So far, submitting the 'employee' login form doesn't submit any form data to anywhere that I can tell. Tried using http_basic_ldap and there seemed to be session conflicts between the two types of users, not exactly sure? Am I going about this from the wrong angle? I know it's a lot I really appreciate any help/suggestions.

Reply

Hey brentmtc!

Ah, sounds cool! Here are some pointers :).

> installed the Ldap package, added a service, a separate user provider, firewall, routes and login form

All good... maybe :). You may not need 2 separate firewalls. It depends on your app: will "normal users" and "ldap users" being viewing the same pages? My guess is "yes". In that case, you only want 1 firewall (only 1 firewall can be active per request, and the active one is chosen by matching a URL to the firewall's pattern, so if you have 2 firewalls, only 1 will be active for a specific page that both users need to go to). Anyways, this is 100% ok to do this - it just means that you have 2 different ways to log into the same security system (i.e. firewall).

> So far, submitting the 'employee' login form doesn't submit any form data to anywhere that I can tell.

I would probably create a custom authenticator for the Ldap authentication. Here's the idea:

* /login is your normal login form. It submits to /login/check (the URLs may not match, I'm just showing an example) and your "login form authenticator" knows to "support" and "act" on this URL.

* /login/employees is your employee login form. It submits to /login/employees/check and your "ldap login form authenticator" knows to support and act on this URL. Inside of this authenticator, you could use an Ldap service that you registered (if you follow the LDAP Symfony docs, registering a service is one of the steps) to find the user information in LDAP. Then you would return a User object (more on this User object below).

You could absolutely also use http_basic_ldap or even form_login_ldap - that gives you more functionality out of the box, I just find them a lot more magic (who is reading my credentials and what does the lookup look like?).

One last point: there is a question of should have have just 1 User class or 2 User classes. Having 2 can complicate things because wherever you have $this->getUser() in your app, you may be getting UserClass1 or UserClass2, and each may have different methods/properties. It depends on your app, but I typically prefer to have just 1 User class, which would be an entity in your case. If you do this, then your "ldap form login authenticator" would do this:

A) Fetch the info from the ldap server
B) Check the database to see if that User is in the database yet (this will make more sense in a second). If it is, just return it (maybe you update some data on it from LDAP if you want).
C) If the user doesn't exist yet, create it, set some data on it and persist/flush to the database. You might even set some custom roles - e.g. ROLE_HUMAN_RESOURCES - based on the permissions that user has in LDAP.

Let me know if this helps!

Cheers!

Reply
Default user avatar

How is the user input on the login form (twig template) validated? If the user input is grabbed by an LoginFormAuthenticator which searches for an user by using a UserProvider, it's essential that the UserProvider doesn't allow SQL injections. Wouldn't it be better to validate the input via FormBuilder instead of using a Twig templalte without validation?

Reply

Hey Nino!

Sorry for my slow reply! Excellent question :).

> If the user input is grabbed by an LoginFormAuthenticator which searches for an user by using a UserProvider, it's essential that the UserProvider doesn't allow SQL injections

You're 100% correct. And the UserProvider (the Doctrine user provider) DOES protect against SQL injections by using the normal prepared statement queries. So, you're covered without thinking about it :). This is actually the same reason that you don't have to worry about SQL injections with forms. The form system will help validate "invalid" input (e.g. putting a string into a number field, hacking a choice element to add an option that doesn't exist, or any of your @Assert\ rules), but the form system in itself doesn't validate against SQL injections. The reason is that... it doesn't need to. When you use the ORM (i.e. when you're querying through your repository [as long as you're using :wildcards and setParameter() in your custom queries] and saving entity objects, everything uses prepared statements).

Let me know if that makes sense!

Cheers!

Reply

Why It's recommended to implement a basic form login, instead of using the formbuilder and render the form via {{form(form)}} etc ??

Reply

Hey Ahmedbhs

That's a good question and the answer is because of a couple of reasons. First, the error messages come from a different part of the framework (Security component), so you can't just execute {{ form_error(form.email) }}
Second, you have to change the name of the CSRF token
and lastly because you may not be using the Form component in your application.

Cheers!

Reply

Then how form errors are displayed in twig ?
Wwhy we have to change the csrf token ?

Reply

Twig grabs the error from the form field which is set after validating the form itself. But, the login action is different, it's intercepted by Symfony. In other to access to the error message, you have to retrieve by calling Symfony\Component\Security\Http\Authentication\AuthenticationUtils::getLastAuthenticationError()

About the CSRF, you don't have to actually change the token but you have to use the same name you used for generating it when check if it's valid in the LoginFormAuthenticator

Reply
Kaizoku Avatar
Kaizoku Avatar Kaizoku | posted 2 years ago

Could we simply use bootstrap4 form theme here ?

# config/packages/twig.yaml
twig:
form_themes: ['bootstrap_4_layout.html.twig']

Reply

Hey Kaizoku,

Yes, you can! But unless you write form's HTML code yourself like we did in this screencast. But if you use Symfony Forms - just specify "form_themes" and Twig will automatically style your form with Bootstrap 4 theme - that's the power of Symfony Form component and its Twig integration :)

Cheers!

Reply
Kamil K. Avatar
Kamil K. Avatar Kamil K. | posted 3 years ago

Could you tell me why i have message "no route found for "GET/login""?

Reply

Hey Kamil,

Hm, try to clear the cache first - does it help? If no, try to debug your routes with the next command:
$ bin/console debug:router

You can even grep by "login" if needed:
$ bin/console debug:router | grep login

Do you see the login route in the output?

Cheers!

Reply
Kamil K. Avatar

Yea it helped. I think that in previous tutorials i didn't have to do that. Thank you so much

Reply

Hey Kamil,

If you're about clearing the cache - yeah, most of the time you don't need to do it when you're in dev env... but depending on whether you use virtualization tool e.g. Docker or no it might be a good idea to start with clearing the cache before thinking about something else :)

Cheers!

Reply

Hi! I'm following this tutorial for my project but after I created the SecurityController all my pages doesn't work anymore. Here is the log:

(5/5) RuntimeError
An exception has been thrown during the rendering of a template ("[Syntax Error] Expected PlainValue, got ''' at position 22 in method App\Controller\SecurityController::login() in C:\Users\Giaco\Desktop\TribunaleMinori\TribunaleMinori\config/routes\../../src/Controller/ (which is being imported from "C:\Users\Giaco\Desktop\TribunaleMinori\TribunaleMinori\config/routes/annotations.yaml"). Make sure annotations are installed and enabled.").

And this happen with every page not only with the /login page, can you help me?

Reply

Hey Giacomo Balloccu

I've seen only that error when you try to use single quotes on annotations, i.e


/**
* @Route('/path')
*/
public function action(){...}

Can you double check that and let me know if that was the case?

Cheers!

Reply
Dinu B. Avatar
Dinu B. Avatar Dinu B. | posted 3 years ago

Hi
It would be nice to see an updated version of this based on "make:auth"

Reply

Hey Dinu Brie!

Yea, I added the fancier version of make:auth to MakerBundle shortly after releasing this tutorial :). But basically, what we are writing by hand in this tutorial is more or less identical to what you can now get from make:auth by select the "form login" option. So, you can simple use that to generate all of this code. Or, you can follow this tutorial if you want to learn a bit more about what that generated code does and how it works :).

Let me know if that makes sense!

Cheers!

Reply
Dinu B. Avatar

thx for reply. U're right regarding make:auth, I wrote that when I was watching that episode and right after that things started to clarify. My coding style it's kinda old school but symfony changed my opinion. Keep doing what u're doing!

Reply

Ha! Awesome! Well, you did it the hard way... I mean the *right* way then - you coded up everything manually, which is the best way to master this stuff in my opinion. Keep up the good work yourself!

Cheers!

Reply
Zoltan K. Avatar
Zoltan K. Avatar Zoltan K. | posted 3 years ago

The promised make command is finally there:
MakerBundle 1.8: Instant User & Login Form Commands
https://symfony.com/blog/ne...

Reply

Hey Zoltan!

Yay! Thanks for sharing the link btw!

Cheers!

Reply
Apr Avatar

Hi again! :)

I have a question for you. In the last tutorials from Symfony 2 and 3, if I'm not wrong we were using the FOSUserBundle to manage this part and it was really helpful.

I used it too this bundle and now I'm trying to migrate from Symfony 3 to Symfony 4. I realised that in Symfony 3 I had to override the basic templates from the FOSUserBundle and as it says in https://symfony.com/doc/mas..., I could replace theses templates locating the new templates inside app/Resources/FOSUserBundle/views/whatever.html.twig.

My confussion comes when I tried to find the new location from theses templates I created and I want to keep using.

In the new file structure from Symfony 4, there is no more the folder project/app/, so I don't know where to locate theses files.

Could you explain me where I should put these files now in the new structure or how I should manage it now?

Reply

Hi Ricard Espinàs Llovet

In Symfony 4 you can override bundle templates by putting them into <your-project>/templates/bundles/<bundle-name>/<template>. For more information check this doc page https://symfony.com/doc/cur...

Hope this will help.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.4
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.0", // v1.7.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.4
    }
}