Loading Users From The Database


In this video we are continuing on with our Registration, and post-registration work flows. There is a somewhat intuitive behavior when creating a Registration work flow, and that's that unless we explicitly write the code to do so, we won't be automatically logged in after successfully registering.

There are some other bits and pieces of configuration that we must attend to. Towards the end of the previous video we had set up our Registration form to correctly submit the user's provided form contents. We could process that data, converting the given plain text password into our desired bcrypt encoded equivalent. We had then set this $password as a property on our Member entity, and saved (persist and flush'ed) this data off to the database.

However, if we were to now try logging in with the very same data we just used to register, we wouldn't be able too. And that's quite odd.

Fortunately, fixing this is straightforward given what we already know.

In a previous video we learned how to manually create hard-coded users. We created a user: admin, who resides in_memory:

# /app/config/security.yml

security:

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt

    providers:
        in_memory:
            memory:
                users:
                    admin:
                        password: $2y$13$C3D/lnwWeh73axMnldcB.euo.Gkv4IThttEFp2.yaEWiIt585zbOa #here
                        roles: 'ROLE_ADMIN'

    # snip

This was nice to begin with, as it meant we didn't need to worry about creating a Doctrine entity, or saving / retrieving information from the database.

But now we have a slightly different problem.

We still want to use our in_memory user information, but we also want to load users from our database.

There's two problems we need to address here. One is immediately obvious, and the second is less obvious but just as important.

Ok, so the first problem is: we need to tell Symfony that we now want to load user information from our database. The good news is, all we need to do here is take what we have already done - the in_memory provider configuration, and tweak it slightly.

# /app/config/security.yml

security:

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt

    providers:
        in_memory:
            memory:
                users:
                    admin:
                        password: $2y$13$C3D/lnwWeh73axMnldcB.euo.Gkv4IThttEFp2.yaEWiIt585zbOa #here
                        roles: 'ROLE_ADMIN'

        # new stuff below

        db_provider:
            entity:
                class: AppBundle:Member
                property: username

    # snip

Let's break this down to make it absolutely clear what's happening here.

Much like the in_memory provider entry, we need some 'top level' entry to describe / identify what the config in this section represents. I've used the key of db_provider, but you can use whatever key you like. I could have called it database_provider, or silly_long_name_for_no_reason_provider. Whatever name you give your provider is needed as part of [yourfirewalls` config]1, e.g.:

# /app/config/security.yml

security:

    # snip

    providers:
        in_memory:
            memory:
                # snip
        db_provider:
            entity:
                class: AppBundle:Member
                property: username

    firewalls:
        dev:
            # snip
        main:
            pattern: ^/
            provider: db_provider # <------ this bit here
            # snip

We're going to come back to this very shortly, because this has introduced the second of the two problems I mentioned above.

Back in our provider config:

# /app/config/security.yml

security:
    providers:
        db_provider:
            entity:
                class: AppBundle:Member
                property: username

The entity key is important.

You cannot (easily) change this. And you likely never will need too.

But to give you a heads up on where this information is coming from, you can see the line by clicking here. Truthfully, you don't need to know this, but I have never liked not knowing where things come from, so wanted to share this with you. If you don't believe me - and that's cool btw, always test this stuff for yourself - change the value entity to entity_key_hack or similar.

This is really easy to do in PHPStorm, just use ctrl+N (or cmd+O on a Mac) and type in DoctrineBundle. PHPStorm will find the file for you.

and you should now see any error like:

Unrecognized option "entity" under "security.providers.db_provider"

Meaning you would now need to change your config to e.g.:

# /app/config/security.yml

security:
    providers:
        db_provider:
            entity_key_hack:
                class: AppBundle:Member
                property: username

Of course, change this back immediately before proceeding. You should never change third party code directly in your project. If you do need to change third party code, fork the code, make your change, and put in a pull request. That's a totally different issue, and again, I'm only showing this to demystify where this stuff comes from.

Back in our config, class should be fairly self explanatory. It is the class name that we are using as our user entity. In our case, this is our Member, and we use the short hand syntax AppBundle: Member to point to this class.

We could also use the long form / fully qualified class name and that would work just fine too:

# /app/config/security.yml

security:
    providers:
        db_provider:
            entity_key_hack:
                class: AppBundle\Entity\Member
                property: username

Finally, property helps Symfony figure out which class property on our Member entity (or whatever entity you have that implements UserInterface in this situation) is the 'username' part of the 'username' / 'password' combo we are supplying.

Typically, this will be username. However, if you are using a different property to represent your user data (e.g. email) then change this up accordingly:

# /app/config/security.yml

security:
    providers:
        db_provider:
            entity_key_hack:
                class: AppBundle\Entity\Member
                property: email

Taking the video content whereby we register a user named q, with the email of q@q.com, and given the above config, we would need to use the username property of q@q.com to log in. Using the username of q in this instance would not work.

Two For The Price Of One

Ok, so that's the first of our two problems sorted. We can now load user's from our database.

However, we have created a second problem for ourselves.

We have our in_memory user provider, and our db_provider user provider.

In our firewall configuration for our main firewall, we are explicitly telling main to use the in_memory user provider for checking valid user data.

Now, the naive solution is to simply change this to db_provider, and that means we can now log in using user information that's stored in the database. But if we do this, of course, we can no longer use our admin user, as that user doesn't reside in the database.

Fortunately, Symfony has us covered here.

What we need to do is create a Chain of user providers. This sounds a little complicated, but is actually really easy to set up.

Think of a chain in real life. It consists of two or more elements linked together. If we start with the first element of our chain, we can look at it, determine if it meets our needs, and if so, all good. If not, we can proceed to the second link in our chain, repeat the process, and so on.

We're going to do this with our user providers.

We can chain two or more of these user providers together, and Symfony will look in each of the given providers until it - hopefully - finds a valid user matching the given credentials:

# /app/config/security.yml

security:

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt
        AppBundle\Entity\Member:
            algorithm: bcrypt

    providers:
        chain_provider: # <-- new
            chain:
                providers: [ in_memory, db_provider ]
        in_memory:
            memory:
                users:
                    admin:
                        password: $2y$13$C3D/lnwWeh73axMnldcB.euo.Gkv4IThttEFp2.yaEWiIt585zbOa
                        roles: 'ROLE_ADMIN'
        db_provider:
            entity:
                class: AppBundle\Entity\Member
                property: username

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        main:
            pattern: ^/
            provider: chain_provider # <-- important

Much like the other (in_memory, db_provider) entries in providers, we need to start by declaring a new key which can be called anything we like. As this section of config represents our chain of user providers, it makes sense to call this key the chain_provider.

Under chain_provider, the key of chain is important. Again, much like with entity under db_provider, this key is expected so we cannot (easily) change it.

Under chain, we have the key of providers, onto which we need to pass in an array. As this is YAML, we can specify arrays either as a dashed list, or in the brackets notation. Watch the video for a better understanding of this if at all unsure (around the 2 minute mark).

Finally - and importantly - we need to update our main firewall to use the chain_provider.

At this point, we should be able to log in as our user admin, and we can log in as one of our database users, but now we get a different error. Ok, a new error is good:

Type error: Argument 4 passed to Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken::__construct() must be of the type array, null given, called in /path/to/my/project/vendor/symfony/symfony/src/Symfony/Component/Security/Core/Authentication/Provider/UserAuthenticationProvider.php on line 96

It might be good that we got this new error, but it seems like a scary one.

Ok, so it's complaining about constructing a UsernamePasswordToken. In particular, it really doesn't like being given null as the fourth argument. It expects an array. Where have we gone wrong?

Let's take a look at the constructor for UsernamePasswordToken to see if that offers any more help. Again, this is really easy to do in PHPStorm, just use ctrl+N (or cmd+O on a Mac) and enter UsernamePasswordToken. Let PHPStorm do all the hard work for you.

// /vendor/symfony/symfony/src/Symfony/Component/Security/Core/Authentication/Token/UsernamePasswordToken.php

class UsernamePasswordToken extends AbstractToken
{
    private $credentials;
    private $providerKey;

    /**
     * Constructor.
     *
     * @param string|object            $user        The username (like a nickname, email address, etc.), or a UserInterface instance or an object implementing a __toString method
     * @param string                   $credentials This usually is the password of the user
     * @param string                   $providerKey The provider key
     * @param (RoleInterface|string)[] $roles       An array of roles
     *
     * @throws \InvalidArgumentException
     */
    public function __construct($user, $credentials, $providerKey, array $roles = array())
    {

Here's a link to this code on GitHub.

Looking at this we can see the fourth argument means $roles.

If we take a look back inside our Member entity as it stands currently, we can see the cause of this problem:

<?php

// /src/AppBundle/Entity/Member.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Member
 *
 * @ORM\Table(name="member")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\MemberRepository")
 */
class Member
{
    // * snip *

    public function getRoles()
    {
        // TODO: Implement getRoles() method.
    }

We should probably implement that method :)

All we need to do here is return an array of one or more strings. The most basic common setup is to give every logged in user the role of ROLE_USER. A role is simply a string starting with ROLE_ and then whatever else you need. If you take WordPress as an example where they have the five default roles of:

  • Administrator
  • Editor
  • Author
  • Contributor
  • Subscriber

We could represent these in Symfony as:

  • ROLE_ADMINISTRATOR (or ROLE_ADMIN)
  • ROLE_EDITOR
  • ROLE_AUTHOR
  • ROLE_CONTRIBUTOR
  • ROLE_SUBSCRIBER

To be absolutely clear, roles can be anything. There's nothing special going on here. ROLE_CHEESE_MAKER or ROLE_CODE_BOFFIN, or literally anything else you like is a valid role.

Going back to our code, we're going to have:

<?php

// /src/AppBundle/Entity/Member.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Member
 *
 * @ORM\Table(name="member")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\MemberRepository")
 */
class Member implements UserInterface, \Serializable
{
    // * snip *

    public function getRoles()
    {
        return [
            'ROLE_USER',
        ];
    }

Simple enough. A short hand array syntax is fine, but the old style array('ROLE_USER') is also valid.

Role Hierarchy

At this point it's worth pointing out that we did give our admin user a role. We gave the admin user the role of ROLE_ADMIN.

It would be nice if ROLE_ADMIN also inherited the role of ROLE_USER without us having to explicitly go and change the admin user setup to define this.

Fortunately, Symfony provides us with the concept of a role_hierarchy. With this config we can define which roles inherit other roles. It sounds a little complex, so a quick example:


# app/config/security.yml

security:
    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER

Here we say if we log in as a user that gets given the role of ROLE_ADMIN, then you will also inherit (or get given) the role of ROLE_USER.

The official docs have more on this, so read up there if unsure.

Are We There Yet?

We've come all this way, but we aren't quite logged in yet. At least, not for our user's provided by the database.

Much like how we hadn't implemented the getRoles method, we also haven't yet implemented serialize and unserialize. As a result, our user data is not being properly saved in the session.

There's two parts to this. First, we need the implementation. Then, we need to understand why the implementation is the way it is.

Let's start with the implementation, as (hopefully) we all love a bit of code:

<?php

// /src/AppBundle/Entity/Member.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Member
 *
 * @ORM\Table(name="member")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\MemberRepository")
 */
class Member implements UserInterface, \Serializable
{
    // * snip *

    public function getRoles()
    {
        return [
            'ROLE_USER',
        ];
    }

    public function serialize()
    {
        return serialize([
            $this->id,
            $this->username,
            $this->password,
        ]);
    }

    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->password
            ) = unserialize($serialized);
    }

Whereas getRoles comes from UserInterface, the serialize and unserialize methods are a requirement of implementing \Serializable.

Note here that \Serializable is a predefined interface - in other words, it's part of PHP itself. Implementing this interface is nothing to do with Symfony.

Inside our serialize method we use the PHP function of serialize to convert our given array (containing id, username, and password) into a funky string that looks something like:

{a:3:{i:0;i:1;i:1;s:1:"q";i:2;s:60:"$2y$13$Fw.LCneWupdmVQjNmM.B6enN1HuJJdr5ytxgJ6W7M/poyfR2CLs9q";}}

Given that we just turned our nice data into the above monstrosity, we better provide a solution for reversing this process. That's the job of unserialize, which again, uses the PHP function of unserialize to get us back to 'normal'.

The use of list here means create an array containing these three properties (id, username, and password) from the outcome of unserializing whatever data gets passed into this function. It's a little confusing, but hopefully it makes a little more sense after seeing the long-winded equivalent:

$unserializedData = unserialize($serialized);

// $unserializedData now contains:
// array(3) {
//    [0]=> int(1)
//    [1]=> string(1) "q"
//    [2]=> string(60) "$2y$13$Fw.LCneWupdmVQjNmM.B6enN1HuJJdr5ytxgJ6W7M/poyfR2CLs9q" 
// }

$outcome = [
    $unserializedData[0],
    $unserializedData[1],
    $unserializedData[2],
];

return $outcome;

When our user logs in, the data we declared in our serialize function is saved off to our user's session. This looks something like this:

_sf2_attributes|a:2:{s:14:"_security_main";s:428:"C:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":340:{a:3:{i:0;N;i:1;s:4:"main";i:2;s:300:"a:4:{i:0;C:23:"AppBundle\Entity\Member":98:{a:3:{i:0;i:1;i:1;s:1:"q";i:2;s:60:"$2y$13$Fw.LCneWupdmVQjNmM.B6enN1HuJJdr5ytxgJ6W7M/poyfR2CLs9q";}}i:1;b:1;i:2;a:1:{i:0;O:41:"Symfony\Component\Security\Core\Role\Role":1:{s:47:"Symfony\Component\Security\Core\Role\Rolerole";s:9:"ROLE_USER";}}i:3;a:0:{}}";}}";s:26:"_csrf/support_request_form";s:43:"kPj1f8eYeaxvb7Ve8J6_LYnvlU57eHJXK6waqBvOu_o";}_sf2_flashes|a:0:{}_sf2_meta|a:3:{s:1:"u";i:1494240477;s:1:"c";i:1494240477;s:1:"l";s:1:"0";}%

If you look closely, you will see our user id, username, and password nestled in there.

Where this becomes more useful is on the second and subsequent requests after the user has logged in. Remember, we don't want to have to log in on every single request.

In order to figure out if we are who we say we are, on future requests this data is deserialized. The id property is used to query for the given user matching that id, and if our data matches then we must be who we say we are. If this data doesn't match then we are logged out.

You can read more about this here.

The truth is, you don't need to know what's happening "underneath the bonnet" so to speak in order to use this on a day-to-day basis. My nature is to investigate how this all works for two reasons. Firstly, I find it interesting. Second, I find it helpful to know what's happening for when things inevitably go wrong.

Anyway, at this point we finally have a working login system for both our in_memory users, and our users loaded from the database. Fantastic.

Code For This Course

Get the code for this course.

Episodes