Dynamic Roles

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

Denying access is great... but we still have a User class that gives every user the same, hardcoded role: ROLE_USER:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 46
public function getRoles()
{
return ['ROLE_USER'];
}
... lines 51 - 93
}

And maybe that's enough for you. But, if you do need the ability to assign different permissions to different users, then we've gotta go a little further.

Let's say that in our system, we're going to give different users different roles. How do we do that? Simple! Just create a private $roles property that's an array. Give it the @ORM\Column annotation and set its type to json_array:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 40
/**
* @ORM\Column(type="json_array")
*/
private $roles = [];
... lines 45 - 110
}

Tip

json_array type is deprecated since Doctrine 2.6, you should use json instead.

This is really cool because the $roles property will hold an array of roles, but when we save, Doctrine will automatically json_encode that array and store it in a single field. When we query, it'll json_decode that back to the array. What this means is that we can store an array inside a single column, without ever worrying about the JSON encode stuff.

Returning the Dynamic Roles

In getRoles(), we can get dynamic. First, set $roles = $this->roles:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 51
public function getRoles()
{
$roles = $this->roles;
... lines 55 - 61
}
... lines 63 - 110
}

Second, there's just one rule that we need to follow about roles: every user must have at least one role. Otherwise, weird stuff happens.

That's no problem - just make sure that everyone at least has ROLE_USER by saying: if (!in_array('ROLE_USER', $roles)), then add that to $roles. Finally, return $roles:

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 51
public function getRoles()
{
$roles = $this->roles;
// give everyone ROLE_USER!
if (!in_array('ROLE_USER', $roles)) {
$roles[] = 'ROLE_USER';
}
return $roles;
}
... lines 63 - 110
}

Oh, and don't forget to add a setRoles() method!

... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 63
public function setRoles(array $roles)
{
$this->roles = $roles;
}
... lines 68 - 110
}

Migration & Fixtures

Generate the migration for the new field:

./bin/console doctrine:migrations:diff

We should double-check that migration, but let's just run it:

./bin/console doctrine:migrations:migrate

Finally, give some roles to our fixture users! For now, we'll give everyone the same role: ROLE_ADMIN:

... lines 1 - 22
AppBundle\Entity\User:
user_{1..10}:
... lines 25 - 26
roles: ['ROLE_ADMIN']

Reload the fixtures!

./bin/console doctrine:fixtures:load

Ok, let's go see if we have access! Ah, we got logged out! Don't panic: that's because our user - identified by its id - was just deleted from the database. Just log back in.

So nice - it sends us back to the original URL, we have two roles and we have access. Oh, and in a few minutes - we'll talk about another tool to really make your system flexible: role hierarchy.

So, how do I Set the Roles?

But now, you might be asking me?

How would I actually change the roles of a user?

I'm not sure though... because I can't actually hear you. But if you are asking me this, here's what I would say:

$roles is just a field on your User, and so you'll edit it like any other field: via a form. This will probably live in some "user admin area", and you'll use the ChoiceType field to allow the admin to choose the roles for every user:

class EditUserFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder)
    {
        $builder
            ->add('roles', ChoiceType::class, [
                'multiple' => true,
                'expanded' => true, // render check-boxes
                'choices' => [
                    'Admin' => 'ROLE_ADMIN',
                    'Manager' => 'ROLE_MANAGER',
                    // ...
                ],
            ])
            // other fields...
        ;
    }
}

If you have trouble, let me know.

What about Groups?

Oh, and I think I just heard one of you ask me:

What about groups? Can you create something where Users belong to Groups, and those groups have roles?

Totally! And FOSUserBundle has code for this - so check it out. But really, it's nothing crazy: Symfony just calls getRoles(), and you can create that array however you want: like by looping over a relation:

class User extends UserInterface
{
    public function getRoles()
    {
        $roles = [];
        
        // loop over some ManyToMany relation to a Group entity
        foreach ($this->groups as $group) {
            $roles = array_merge($roles, $group->getRoles());
        }
        
        return $roles;
    }
}

Or just giving people roles at random.

Leave a comment!

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
        "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
    }
}