Buy

Adding Dynamic Roles to each User

Right now, all users get just one role: ROLE_USER, because it’s what we’re returning from the getRoles() function inside the User entity.

Add a roles field and make it a json_array type:

// src/Yoda/UserBundle/Entity/User.php
// ...

/**
 * @ORM\Column(type="json_array")
 */
private $roles = array();

json_array allows us to store an array of strings in one field. In the database, these are stored as a JSON string. Doctrine takes care of converting back and forth between the array and JSON.

Now, just update the getRoles() method to use this property and add a setRoles method:

public function getRoles()
{
    return $this->roles;
}

public function setRoles(array $roles)
{
    $this->roles = $roles;

    // allows for chaining
    return $this;
}

Cool, except the way it’s written now, a user could actually have zero roles. Don’t let this happen: that user will become the undead and cause a zombie uprising. They can login, but they won’t actually be authenticated. You’ve been warned.

Be a hero by adding some logic to getRoles(). Let’s just guarantee that every user has ROLE_USER:

public function getRoles()
{
    $roles = $this->roles;
    $roles[] = 'ROLE_USER';

    return array_unique($roles);
}

Update the SQL for the new field and then head back to the fixture file:

php app/console doctrine:schema:update --force

Let’s copy the code here and make a second, admin user. Give this powerful Imperial user ROLE_ADMIN:

// src/Yoda/UserBundle/DataFixtures/ORM/LoadUsers.php
// ...

public function load(ObjectManager $manager)
{
    // ...
    $manager->persist($user);

    $admin = new User();
    $admin->setUsername('wayne');
    $admin->setPassword($this->encodePassword($admin, 'waynepass'));
    $admin->setRoles(array('ROLE_ADMIN'));
    $manager->persist($admin);

    $manager->flush();
}

Let’s reload the fixtures!

php app/console doctrine:fixtures:load

Now, when we login as admin, the web debug toolbar shows us that we have ROLE_USER and ROLE_ADMIN.

Using the AdvancedUserInterface for Inactive Users

Could we disable users, like if they were spamming our site? Well, we could just delete them and send a strongly-worded email, but yea, we can also disable them!

Add an isActive boolean field to User. If the field is false, it will prevent that user from authenticating. Don’t forget to add the getter and setter methods either by using a tool in your IDE or by re-running the doctrine:generate:entities command:

// src/Yoda/UserBundle/Entity/User.php
// ...

/**
 * @var bool
 *
 * @ORM\Column(type="boolean")
 */
private $isActive = true;

// ...
// write or generate your getIsActive and setIsActive methods...

After that, update our schema to add the new field:

php app/console doctrine:schema:update --force

So the isActive field exists, but it’s not actually used during login. To make this work, change the User class to implement AdvancedUserInterface instead of UserInterface:

// src/Yoda/UserBundle/Entity/User.php
// ...

use Symfony\Component\Security\Core\User\AdvancedUserInterface;

class User implements AdvancedUserInterface
{
    // ...
}

Tip

For the OO geeks, AdvancedUserInterface extends UserInterface.

The new interface is a stronger version of UserInterface that requires four additional methods. I’ll use my IDE to generate these. If any of these methods return false, Symfony will block the user from logging in. To prove this, let’s make them all return true except for isAccountNonLocked:

// src/Yoda/UserBundle/Entity/User.php
// ...

public function isAccountNonExpired()
{
    return true;
}

public function isAccountNonLocked()
{
    return false;
}

public function isCredentialsNonExpired()
{
    return true;
}

public function isEnabled()
{
    return true;
}

Logging in now is less fun: we’re blocked with a helpful message.

Each of these methods does the exact same thing: they block login. Each will give the user a different message, which you can translate if you want. Set each to return true, except for isEnabled. Let’s have it return the value for our isActive property:

// src/Yoda/UserBundle/Entity/User.php
// ...

public function isAccountNonLocked()
{
    return true;
}

public function isEnabled()
{
    return $this->getIsActive();
}

If isActive is false, this should prevent the user from logging in.

Head over to our user fixtures so we can try this. Set the admin user to inactive:

// src/Yoda/UserBundle/DataFixtures/ORM/LoadUsers.php
// ...

public function load(ObjectManager $manager)
{
    // ...
    $admin->setIsActive(false);
    // ...
}

Next, reload your fixtures:

php app/console doctrine:fixtures:load

When we try to login, we’re automatically blocked. Cool! Let’s remove the setIsActive call we just added and reload the fixtures to put everything back where it started.

Leave a comment!

  • 2017-05-17 weaverryan

    Woohoo! So happy to hear it! And thanks for posting your final code here for others :). I'm just sad I didn't *actually* get a pokémon meme ;).

    Cheers!

  • 2017-05-17 Daniel Larusso

    Hey weaverryan it was super effective (add "super effective" pokémon meme here)
    I moved the relationship to a new $roleCollection property. (also added add, get & remove method)

    at least I've changed getRoles() to


    /**
    * @return Role[]
    */
    public function getRoles()
    {
    $roles = array();
    foreach ($this->roleCollection as $role) {
    $roles[] = $role->getDescription();
    }

    return $roles;
    }

    Thank you ryan :)

  • 2017-05-15 weaverryan

    Hey Daniel Larusso!

    Ah, I know the problem :). When you finish authentication, the guard authenticator calls $user->getRoles() on your User object. This must return an *array* of roles. But with Doctrine, a relationship returns an ArrayCollection *object*. Here's what you should do:

    A) Move your relationship to some other property - e.g. $roleObjects (I can't think of a better name). Obviously, also change your getter & setter to be getRoleObjects/setRoleObjects
    B)Add a new getRoles() method that returns a true array:


    public function getRoles()
    {
    $roles = [];
    foreach ($this->roleObjects as $roleObject) {
    // I'm assuming your have a getName() method on your object, update as needed
    $roles[] = $roleObject->getName();
    }

    return $roles;
    }

    Cheers!

  • 2017-05-11 Daniel Larusso

    Instead of keeping the roles in an json array, I would like to store them in an own role entity. I've tried it with an ManyToMany relation between User and Role entity. but if I try to login, symfony yells at me like "Type error: Argument 3 passed to Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken::__construct() must be of the type array, object given, called in /var/www/html/vendor/symfony/symfony/src/Symfony/Component/Security/Guard/AbstractGuardAuthenticator.php on line 38"

    I don't know how I could handle this...

  • 2016-10-30 Max

    Wow, I never would have thought about this one :D Another amazing answer - thank you!!

  • 2016-10-30 weaverryan

    Yo Max!

    Another great question! It's optional, but for chaining if you're into that sort of thing!


    // if your setter methods return nothing (no return statement)
    $user->setRoles(array('ROLE_ADMIN'));
    $user->setEmail('foo@example.com');
    $user->setFirstName('Max');

    // if your setter methods return $this
    $user->setRoles(array('ROLE_ADMIN'))
    ->setEmail('foo@example.com')
    ->setFirstName('Max');

    So, it's for syntactic sugar! I don't typically do this, mostly because I don't often find myself calling many setters in a row like this. But that doesn't make it wrong - just a preference :).

    Cheers!

  • 2016-10-30 Max

    Hey!
    I wondered why setter-functions always end with a "return $this" statement and thus return the whole object again? $this->roles = $roles seems to be sufficient to set the property. Thanks in advance :)