Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

The LoginFormAuthenticator

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

To use Guard - no matter what crazy authentication system you have - the first step is always to create an authenticator class. Create a new directory called Security and inside, a new class: how about LoginFormAuthenticator:

... lines 1 - 2
namespace AppBundle\Security;
... lines 4 - 7
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 12 - 30
}

The only rule about an authenticator is that it needs to extend AbstractGuardAuthenticator. Well, not totally true - if you're building some sort of login form, you can extend a different class instead: AbstractFormLoginAuthenticator - it extends that other class, but fills in some details for us.

Hit Command+N - or go to the "Code"->"Generate" menu - choose "Implement Methods" and select the first three:

... lines 1 - 4
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
... lines 8 - 9
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
public function getCredentials(Request $request)
{
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
}
public function checkCredentials($credentials, UserInterface $user)
{
}
... lines 23 - 30
}

Then, do it again, and choose the other two:

... lines 1 - 9
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 12 - 23
protected function getLoginUrl()
{
}
protected function getDefaultSuccessRedirectUrl()
{
}
}

Tip

Starting in Symfony 3.1, you won't see getDefaultSuccessRedirectUrl() in this list anymore. Don't worry! We'll tell you how to handle this later.

That was just my way to get these methods in the order I want, but it doesn't matter.

How Authenticators Work

When we're finished, Symfony will call our authenticator on every single request. Our job is to:

  1. See if the user is submitting the login form, or if this is just some random request for some random page.
  2. Read the username and password from the request.
  3. Load the User object from the database.

getCredentials()

That all starts in getCredentials(). Since this method is called on every request, we first need to see if the request is a login form submit. We setup our form so that it POSTs right back to /login. So if the URL is /login and the HTTP method is POST, our authenticator should spring into action. Otherwise, it should do nothing: this is just a normal page.

Create a new variable called $isLoginSubmit Set that to $request->getPathInfo() - that's the URL - == '/login' && $request->isMethod('POST'):

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 20
public function getCredentials(Request $request)
{
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST');
... lines 24 - 34
}
... lines 36 - 51
}

Tip

Instead of hardcoding the /login URL, you could instead check for the current page's route name: if ($request->attributes->get('_route') === 'security_login' && $request->isMethod('POST'))

If both of those are true, the user has just submitted the login form.

So, if (!$isLoginSubmit), just return null:

... lines 1 - 22
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST');
if (!$isLoginSubmit) {
// skip authentication
return;
}
... lines 28 - 53

If you return null from getCredentials(), Symfony skips trying to authenticate the user and the request continues on like normal.

getCredentials(): Build the Form

If the user is trying to login, our new task is to fetch the username & password and return them.

Since we built a form, let's let the form do the work for us.

Normally in a controller, we call $this->createForm() to build the form:

... lines 1 - 38
abstract class Controller implements ContainerAwareInterface
{
... lines 41 - 274
/**
* Creates and returns a Form instance from the type of the form.
*
* @param string|FormTypeInterface $type The built type of the form
* @param mixed $data The initial data for the form
* @param array $options Options for the form
*
* @return Form
*/
protected function createForm($type, $data = null, array $options = array())
{
return $this->container->get('form.factory')->create($type, $data, $options);
}
... lines 288 - 396
}

In reality, this grabs the form.factory service and calls create() on it.

Dependency Inject form.factory (FormFactory)

So how can we create a form in the authenticator? Use dependency injection to inject the form.factory service.

Add a __construct() method with a $formFactory argument:

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 15
public function __construct(FormFactoryInterface $formFactory)
{
... line 18
}
... lines 20 - 51
}

Now, I like to type-hint my arguments, so let's just guess at the service's class name and see if there's one called FormFactory. Yep, there's even a FormFactoryInterface!

... lines 1 - 5
use Symfony\Component\Form\FormFactoryInterface;
... lines 7 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 15
public function __construct(FormFactoryInterface $formFactory)
{
... line 18
}
... lines 20 - 51
}

That's probably what we want. I'll press Option+Enter and select "Initialize Fields" to set that property for me:

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
private $formFactory;
public function __construct(FormFactoryInterface $formFactory)
{
$this->formFactory = $formFactory;
}
... lines 20 - 51
}

If you're still getting used to dependency injection and that all happened too fast, don't worry. We know we want to inject the form.factory service, so I guessed its class for the type-hint, which is optional. You can always find your terminal and run:

./bin/console debug:container form.factory

to find out the exact class to use for the type-hint. We will also register this as a service in services.yml in a minute.

Return the Credentials

Back in getCredentials(), add $form = $this->formFactory->create() and pass it LoginForm::class:

... lines 1 - 4
use AppBundle\Form\LoginForm;
... lines 6 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 20
public function getCredentials(Request $request)
{
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST');
if (!$isLoginSubmit) {
// skip authentication
return;
}
$form = $this->formFactory->create(LoginForm::class);
... lines 30 - 34
}
... lines 36 - 51
}

Then - just like always - use $form->handleRequest($request):

... lines 1 - 22
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST');
if (!$isLoginSubmit) {
// skip authentication
return;
}
$form = $this->formFactory->create(LoginForm::class);
$form->handleRequest($request);
... lines 31 - 53

Normally, we would check if $form->isValid(), but we'll do any password checking or other validation manually in a moment. Instead, just skip to $data = $form->getData() and return $data:

... lines 1 - 22
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST');
if (!$isLoginSubmit) {
// skip authentication
return;
}
$form = $this->formFactory->create(LoginForm::class);
$form->handleRequest($request);
$data = $form->getData();
return $data;
... lines 35 - 53

Since our form is not bound to a class, this returns an associative array with _username and _password. And that's it for getCredentials(). If you return any non-null value, authentication continues to the next step.

Leave a comment!

76
Login or Register to join the conversation
Default user avatar
Default user avatar Terry Caliendo | posted 5 years ago

At about the 2 minute mark, you hard code "/login". It would be better to somehow call the path "security_login" created in the routing of the controller, correct? In case "/login" later changes to something else.

2 Reply

Hey Terry,

I think you're right. This place can be error prone in future if the login URL changes. In the next chapter we inject a router into the authenticator, so you can easily reuse it to generate the URL.

Cheers!

1 Reply
Default user avatar
Default user avatar Geoff Maddock | posted 5 years ago

I want to use different rules for authentication depending on which subdomain the user is logging in from.

What's the best way to get the subdomain inside of the LoginFormAuthenticator?

2 Reply

Hey Geoff,

As you probably know, $_SERVER does not contain any information about subdomains, but you can get the entire host from it. So, from Symfony's Request object, you can get the host like: $request->server->get('HTTP_HOST'). And if you know the main domain - then with a PHP string function you can easily get the subdomain.

Cheers!

Reply
Default user avatar
Default user avatar Dominik | posted 5 years ago

Hello!
First things first:
In script it is: FormLoginAuthenticator and in code's script: LoginFormAuthenticator

The second problem is with Code->generate:
I don't have getDefaultSuccessRedirectUrl(); It's weird. Is it some kind of bug?

1 Reply

Hey, Dominik!

Thanks for let us know, I've fixed wrong authenticator name right now in 1f26635.

Ah, looks like Symfony had some changes due to deprecations in AbstractFormLoginAuthenticator since Symfony 3.1. Did you upgrade to 3.1? What Symfony version do you use?

Cheers!

Reply

Ah yes, I think you're right about 3.1 Victor.

So, in 3.1, you can still override getDefaultSuccessRedirectUrl(), but you'll need to add the method to your authenticator yourself (if you go to Code -> Generate in PHPstorm, it won't be added for you anymore).

But more long-term, we removed this method (actually, I did it - it's my fault! https://github.com/symfony/symfony/pull/18135), and instead we want you to add a different method - called onAuthenticationSuccess(). If you're in Symfony 3.1, you should be able to do this:


use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\HttpFoundation\RedirectResponse;

class LoginFormAuthentication extends AbstractFormLoginAuthenticator
{
    use TargetPathTrait;

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        // if the user hits a secure page and start() was called, this was
        // the URL they were on, and probably where you want to redirect to
        $targetPath = $this->getTargetPath($request->getSession(), $providerKey);
  
        if (!$targetPath) {
             $targetPath = $this->router->generate('homepage');
        }
 
        return new RedirectResponse($targetPath);
    }
}

Basically, onAuthenticationSuccess() is really the method in charge of things. Previously (and it still works, but is deprecated in 3.1), you could just implement getDefaultSuccessRedirectUrl(), and this method would be taken care of for you. Feel free to do either (use getDefaultSuccessRedirectUrl() or the above code) - both will work, but the above code will be the way forward when Symfony 4 is released in a few years.

Cheers!

3 Reply
Default user avatar

Thank you guys for reply!

I'm using symfony 3.1 but for this tutorial i will use the getDefaultSuccessRedirectUrl() method.

As you said it is still few years to symfony 4 ;)

Cheers!

Reply

Cheers back! Thanks for bringing this to our attention - I'm sure this conversation will help others with the same question :)

Reply
Default user avatar

Why haven't you updated the course yet?

Reply

Yo Tony C!

It's not an accident :). Thanks to Symfony's backwards-compatibility promise, the way that's shown in the screencast still works, and will continue to work until Symfony 4.0. I'm actually the one who made this change to Symfony, so I'm well aware of it! We update the screencasts once a new major version of Symfony comes out - it's our way of balancing keeping things up to date, but not constantly re-recording things for tiny changes. It's not a perfect system :). Symfony's backwards compatibility promise helps us (and developers) out a lot with this!

But, in hindsight, I do think a note is in order - if you code with the tutorial perfectly, there's no problems. But if you use the PHPStorm shortcuts for "implement methods", then you won't see all the methods that we get in the recording - and that can be confusing indeed!

Cheers!

Reply
Default user avatar

Well that's good to know! Thank you!

Backwards compatibility... a blessing and a curse all rolled up in one idea.

Reply

Haha, you nailed it - every time Symfony makes a change, I rejoice... and I cringe :D. I just added an issue internally to add a note to this chapter and the next (next chapter is where we actually code up this method).

Cheers!

Reply
Default user avatar

can you update this for Symfony 4 ? ( in a comment :) )

Reply

Hey cybernet2u!

Haha, well, which part exactly? If you're using the LoginFormAuthenticator, the change is here: https://knpuniversity.com/screencast/symfony-security/login-form-authenticator#comment-2760429219. You'll also need to add a new supports() method, which does part of the job of getCredentials() in 4. So, like this:


    public function supports(Request $request)
    {
        // if this returns true, then getCredentials() is called
        return $request->getPathInfo() == '/login' && $request->isMethod('POST');
    }

    public function getCredentials(Request $request)
    {
        // no need to check that this is the login page anymore - it's done above 
        $form = $this->formFactory->create(LoginForm::class);
        $form->handleRequest($request);
        $data = $form->getData();

        return $data;
    }

Cheers!

Reply
Yahya E. Avatar
Yahya E. Avatar Yahya E. | weaverryan | posted 5 years ago | edited

For supports() function, it might be useful to use this line:


    public function supports(Request $request)
    {
        // if this returns true, then getCredentials() is called
        return $request->getPathInfo() == $this->router->generate('security_login') && $request->isMethod('POST');
    }

It might save us breaking something if we want to change /login path, and <b>especially</b> if we are dealing with multi-lingual applications :)

Reply

Hey Yahya E.
It's not common to change the login's path name, but I totally agree with you, that code is more bullet proof

Cheers!

Reply
Dirk Avatar

Hi Ryan, I have implemented this and it works beautifully however I have the impression that "getCredentials" no longer is called on every request... Could this be the case?

I'm want to update a custom field "last_access" on the user entity with a datetimestamp so I can keep track of who was online the last 5 minutes. I figured "getCredentials" would be the perfect place for that but unfortunately it does not get called (only at login).

Reply

Hey Dirk!

Ah yes, you're right! Once you update to the new Symfony 4 way (i.e. with the supports()) method, then supports() IS called on every request. But, now, getCredentials() is only called when supports() returns true. Actually, this is the whole purpose of supports(): Symfony is asking "Does your authenticator support trying to authenticate this request". If false is returned, getCredentials() is never called.

So, technically speaking, supports() would be the better place to move your last_access logic. But.... putting this logic here is not really the best spot. Simply because your authenticator is all about authentication, and updating this "last_access" is a totally different thing. Instead, I would create an "event susbcriber" that listens on the kernel.request event. This will have a similar effect: the method in your subscriber will be called on every request, and you can do whatever you want. By injecting the Security class (https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Security.php) into your event subscriber, you can easily get the User object so that you can update their field.

P.S. If you're using the MakerBundle on a Symfony 4 project, you can even generate a subscriber with:


php bin/console make:subscriber

Cheers!

Reply
Dirk Avatar

Thanks Ryan! I did create an event subscriber and it works great! However, I was wondering how I can make sure that this is the last subscriber to be launched. The reason is that I'm updating the database with flush() and I do not want to launch other doctrine transactions prematurely... Is lowering the priority of the event an option? If yes, to what level?

Reply

Hey Dirk,

Yes, I think priority is an option for you. The priorities of the internal Symfony listeners usually range from -255 to 255 but your own listeners can use any positive or negative integer, so I suppose you can use a number which is lower than -255 to be sure your subscriber will be called *after* any Symfony one. And of course, make sure all your other listeners do not have a lower priority than the priority for that last subscriber. But it also sounds like a good spot to add some integration tests, at least to make sure everything works and you won't get any exceptions.

Cheers!

Reply
Dirk Avatar

Great! Thanks Victor, so far it works as expected with a priority of -300.

Best regards,

Dirk

Reply
Default user avatar

that tutorial i already followed, and even if the form was submitted, symfony never saw me as authenticated :( ( i did use the supports method )... now it's fixed, with other tutorial

Reply

Yo cybernet2u!

Awesome :). Well, I'm happy it's fixed now :).

Reply
Default user avatar
Default user avatar JSThePatriot | weaverryan | posted 5 years ago | edited

weaverryan In the original implementation that is now deprecated you had the following lines that you no longer have... should we still have them, or do like the above and leave them out?


    $targetPath = null;

    // This comment wasn't in there, but the contents of this if is your $targetPath = line.
    if ($request->getSession() instanceof SessionInterface) {}
Reply

Yo JSThePatriot!

Ah, interesting! That was a line that I didn't originally add to AbstractFormLoginAuthenticator, but I see it! In the framework (unless you're doing something crazy), you shouldn't need this. In theory, you could configure things to not have a Session object at all - so getSession() would return null (I'm not sure why they did it as an instanceof check, but they're basically checking to see if the Session exists). Feel free to keep it in, but you won't need it.

Good question man! Cheers!

1 Reply
odds Avatar

Hey Ryan,

It all works until... onAuthenticationSuccess has to be called. If it isn't there, explosion. If it is there, no authentication required and I get taken to the page I requested, behind the firewall...

Reply

Hey odds!

Hmm. So if you're on Symfony 3.1, then you should have onAuthenticationSuccess, but having it is technically optional until Symfony 4 (if you don't have it, you'll just receive a warning). But in either case, that method should have nothing to do with whether or not authentication is required for some endpoint - that's very strange. Let's debug!

A) If you don't have onAuthenticationSuccess, what is the error?
B) When you are taken to the page where no authentication is required, what do you see in the web debug toolbar for security? Does it say "anon"? And what code should be forcing authentication on that page? Do you have an access_control or a denyAccessUnlessGranted in a controller?

Let me know - we'll figure it out!

Cheers!

Reply
odds Avatar

Hey Ryan,

I was a little bit to early with my conclusion. It works, but the toolbar does not show my username. That's why I thought something went horribly wrong...

Now trying to figure out why the toolbar shows n/a instead of my username...

Reply

Hey odds!

Ah, fascinating! The toolbar shows n/a? Hmm, what does your firewall look like? Usually, that toolbar only either shows (A) your username or (B) anon, if you're not logged in. n/a almost makes me think that the URL you're visiting isn't covered by your firewall. But that's just a first guess!

Cheers!

1 Reply
Default user avatar
Default user avatar Attila László | weaverryan | posted 5 years ago | edited

Hey Ryan!

I coded everything by the new updated way. So I'm on the login page and after I hit submit, it does nothing and always redirects back to the login page. It doesn't matter if using the correct password or a wrong one.

In the profiler the POST values are there, but in the form section it shows null.

UPDATE: After some extensive google search I got it working.


public function getCredentials(Request $request)
    {
        $isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST');

        if ($isLoginSubmit) {

            $req = $request->request->get('login_form');

            return [
                '_username' => $req['_username'],
                '_password' => $req['_password'],
            ];
        }

        $form = $this->formFactory->create(LoginForm::class);
        $form->handleRequest($request);

        $data = $form->getData();

        dump($data);

        return $data;
    }
Reply

Hey Attila László!

Ah, awesome! Usually, when you submit and see nothing it's either because your authenticator is not being called (i.e. it's not configured correctly in security.yml) or you have a bug in your getCredentials() method, so that it is not returning the credentials when it should.

In this case, it looks like you basically added some code to skip the form framework and instead grab the POST information directly from the request. That's a great way to do it, and actually, I wish I had done it this way - it's a bit simpler with no downside. You should be able to simplify your code:


public function getCredentials(Request $request)
    {
        $isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST');

        if ($isLoginSubmit) {

            $req = $request->request->get('login_form');

            return [
                '_username' => $req['_username'],
                '_password' => $req['_password'],
            ];
        }

        // just return null: do no processing on this request
        return;
    }

I'm not sure what the problem was in your case... the form should have been processing the data just fine. But, I like your approach better anyways :).

Cheers!

Reply
Default user avatar
Default user avatar Attila László | weaverryan | posted 5 years ago

Thx for the reply!

These are the files in question:

- https://pastebin.com/BTaQAEtX
- https://pastebin.com/CG1ET7CH
- https://pastebin.com/FWxzcz1m
- https://pastebin.com/TKfFzQhW

Hope we can figure it out, because I really want know these things.

Reply

Hey!

Can you post your login.html.twig and LoginFrom.php file also?

Cheers!

Reply

Hey Attila László!

It's still a mystery to me! You're rendering your form correctly and handling the request in the authenticator just fine. From everything I can see, it *should* be properly processing the data through the form! So, I'm sorry I can't give you an answer! As I mentioned earlier, the way that does *not* use the form in the getCredentials() method is a little simpler/nicer anyways in my opinion (and I'll use that way personally on the future). So, stick with that :).

Cheers!

Reply
odds Avatar

Sjeeez, it was waaay more basic... forgot the 'return' in getUsername()...

Thanks anyway!

Reply
Default user avatar

And is this a public or protected function? Since getDefaultSuccessRedirectUrl was protected...

Reply
Default user avatar

Well, that's one question answered. This method should definitly be public (Symfony says so ;-) ), but than the shit hits the fan. The method getTargetPath is private (big explosion) and getDefaultSuccessRedirectUrl is not found... So, the stuff from the video is out of date and the stuff in the comment is incomplete. Bummer...

Reply

Hey Hermen!

The getTargetPath function comes from the TargetPathTrait. Make sure you're "using" this in your class. And you're right that getTargetPath is private! But that's ok - it's legal to use a private method from a trait (effectively, when you "use" a trait, its methods are copied into your class - so you CAN access private methods).

Let me know if that helps!

Reply
Peter Avatar
Peter Avatar Peter | weaverryan | posted 5 years ago | edited

I just looked at the TargetPathTrait and it only has 3 functions, Save, Get and Remove. The getDefaultSucessRedirectUrl has been removed - v3.2.3.
Am I looking at the wrong spot? or should I just do the redirect myself (Figured what the hell, just create it until I get a response :P )?

Reply

Peter Ah, you're right! I mis-spoke in my previous message! Basically, the solution in the video works, but we made some changes in Symfony, so the new "non-deprecated" version is available above in my comment: https://knpuniversity.com/screencast/symfony-security/login-form-authenticator#comment-2760429219

Basically, you should now implement onAuthenticationSuccess yourself, and inside, you'll make use of getTargetPath(). If you do this, then you don't need the getDefaultSuccessRedirectUrl at all :). But, I can see now that my code-block above is mis-leading on this! So, Peter, you did the right thing by adding getDefaultScucesRedirectUrl yourself, but it's actually not needed. Check (in about 2 minutes) my updated code above the comment I linked to :).

Cheers!

1 Reply

And actually, I had some misleading code in my comment above! You should not need to call getDefaultSuccessRedirectUrl at all anymore in the new way - my misleading code is what confused you on this! I've just updated the original code in my comment above with the not-misleading, proper way.

Cheers!

Reply
Default user avatar
Default user avatar Eddy de Boer | Dominik | posted 5 years ago

Thx for the question, I had the same problem.

Reply
Yahya E. Avatar
Yahya E. Avatar Yahya E. | posted 5 years ago | edited

there Thank you for keeping the tutorial updated with new Symfony releases.

1 Reply
MolloKhan Avatar MolloKhan | SFCASTS | posted 5 years ago | edited

Hey there

is this due to the version of symfony? It may... How are you injecting the "formFactory" argument?

Reply
Pamou M. Avatar

Thank you Diego, Yeah absolutely I have a problem with injection 'autowire'.

thank you very much.

Reply

In that case you have to explicitly enable it by adding autowire: true in your service definition.

Reply
Default user avatar
Default user avatar Mark Ernst | posted 5 years ago

I must be missing something. I've been following the course with 3.3.12 and up until the autowiring, which I skipped (since 3.3.12 does that for me). Now I'm getting the error "Class Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator not found" for some reason. If I add 'Security' to my exclude pattern, the service cannot be found ("exclude: '../../src/AppBundle/{Entity,Repository,Tests,Security}'"). I'm currently at a loss as how this is occurring...

Reply
Default user avatar

Alright. I am that guy that honestly posts after 1,5 hour of searching and after hitting enter finds out that he corrupted the AbstractFormLoginAuthenticator class @trigger_error portion by reading up and removing the semicolon.

Eventually I also found out that declaring _defaults in the services.yml doesn't work globally. I'm also using AppBundle specific Resources which also include a set of services (in the .yml file). Upon copying those defaults introduced in 3.3 it all worked like a charm.

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice