Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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

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

Login Subscribe

I want to create something new: a new user account page. Find your terminal and run:

php bin/console make:controller

Create a new AccountController. Open that up:

... lines 1 - 2
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
class AccountController extends AbstractController
{
/**
* @Route("/account", name="account")
*/
public function index()
{
return $this->render('account/index.html.twig', [
'controller_name' => 'AccountController',
]);
}
}

Perfect! A new /account page, which we can see instantly if we go to that URL.

Change the route name to app_account to be consistent with our other code:

... lines 1 - 7
class AccountController extends AbstractController
{
/**
* @Route("/account", name="app_account")
*/
public function index()
{
... lines 15 - 17
}
}

And, I'm not going to pass any variables to the template for now:

... lines 1 - 7
class AccountController extends AbstractController
{
/**
* @Route("/account", name="app_account")
*/
public function index()
{
return $this->render('account/index.html.twig', [
]);
}
}

Open that: templates/account/index.html.twig. Let's customize this just a bit: Manage Account and an h1: Manage your Account:

{% extends 'base.html.twig' %}
{% block title %}Manage Account!{% endblock %}
{% block body %}
<h1>Manage Your Account</h1>
{% endblock %}

That's pretty boring... but it should be enough for us to get into trouble!

Check if the User Is Logged In

Ok: I only want this page to be accessible by users who are logged in. Log out and then go back to /account. Obviously, right now, anybody can access this. Hmm: we do already know how to require the user to have a specific role to access something - like in CommentAdminController where we require ROLE_ADMIN:

... lines 1 - 11
/**
* @IsGranted("ROLE_ADMIN")
*/
class CommentAdminController extends Controller
{
... lines 17 - 35
}

But... how can we make sure that the user is simply... logged in?

There are actually two different ways. I'll tell you about one of those ways later. But right now, I want to tell you about the easier of the two ways: just check for ROLE_USER.

Above the AccountController class - so that it applies to any future methods - add @IsGranted("ROLE_USER"):

... lines 1 - 4
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
... lines 6 - 8
/**
* @IsGranted("ROLE_USER")
*/
class AccountController extends AbstractController
{
... lines 14 - 22
}

So... why is this a valid way to check that the user is simply logged in? Because, remember! In User, our getRoles() method is written so that every user always has at least this role:

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 66
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
... lines 78 - 128
}

If you are logged in, you definitely have ROLE_USER.

Refresh the page now: it bumps us to the login page. Log in with password engage and... nice! We're sent back over to /account. Smooth.

Adding Admin Users

At this point, even though we're requiring ROLE_ADMIN in CommentAdminController, we... well... don't actually have any admin users! Yep, nobody can access this page because nobody has ROLE_ADMIN!

To make this page... um... actually usable, open src/DataFixtures/UserFixture.php. In addition to these "normal" users, let's also create some admin users. Copy the whole createMany() block and paste below. Give this set of users a different "group name" - admin_users:

... lines 1 - 8
class UserFixture extends BaseFixture
{
... lines 11 - 17
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) {
... lines 21 - 30
});
$this->createMany(3, 'admin_users', function($i) {
... lines 34 - 44
});
$manager->flush();
}
}

Remember: this key is not important right now. But we can use it later in other fixture classes if we wanted to "fetch" these admin users and relate them to different objects. We'll see that later.

Let's create three admin users. For the email, how about admin%d@thespacebar.com:

... lines 1 - 8
class UserFixture extends BaseFixture
{
... lines 11 - 17
protected function loadData(ObjectManager $manager)
{
... lines 20 - 32
$this->createMany(3, 'admin_users', function($i) {
$user = new User();
$user->setEmail(sprintf('admin%d@thespacebar.com', $i));
... lines 36 - 44
});
... lines 46 - 47
}
}

The first name is fine and keep the password so that I don't get completely confused. But now add $user->setRoles() with ROLE_ADMIN:

... lines 1 - 8
class UserFixture extends BaseFixture
{
... lines 11 - 17
protected function loadData(ObjectManager $manager)
{
... lines 20 - 32
$this->createMany(3, 'admin_users', function($i) {
$user = new User();
$user->setEmail(sprintf('admin%d@thespacebar.com', $i));
$user->setFirstName($this->faker->firstName);
$user->setRoles(['ROLE_ADMIN']);
$user->setPassword($this->passwordEncoder->encodePassword(
$user,
'engage'
));
return $user;
});
... lines 46 - 47
}
}

Notice that I do not also need to add ROLE_USER: the getRoles() method will make sure that's returned even if it's not stored in the database:

... lines 1 - 10
class User implements UserInterface
{
... lines 13 - 66
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
... lines 78 - 128
}

Let's reload those fixtures!

php bin/console doctrine:fixtures:load

When that finishes, move over and go back to /login. Log in as one of the new users: admin2@thespacebar.com, password engage. Then, try /admin/comment. Access granted! Woohoo! And we, of course, also have access to /account because our user has both ROLE_ADMIN and ROLE_USER.

Checking for Roles in Twig

Oh, and now that we know how to check if the user is logged in, let's fix our user drop-down: we should not show the login link once we're logged in.

In PhpStorm, open templates/base.html.twig and scroll down a bit. Earlier, when we added the login link, we commented out our big user 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>

Now, we can be a bit smarter. Copy that entire section: we will show it when the user is logged in.

Oh, but how can we check if the user has a role from inside Twig? With: is_granted(): if is_granted('ROLE_USER'), else - I'll indent my logout link - and endif:

<!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">
{% if is_granted('ROLE_USER') %}
... lines 37 - 46
{% else %}
<li class="nav-item">
<a style="color: #fff;" class="nav-link" href="{{ path('app_login') }}">Login</a>
</li>
{% endif %}
</ul>
</div>
</nav>
... lines 55 - 72
</body>
</html>

Inside the if, paste the drop-down code:

<!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">
{% if is_granted('ROLE_USER') %}
<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>
{% else %}
<li class="nav-item">
<a style="color: #fff;" class="nav-link" href="{{ path('app_login') }}">Login</a>
</li>
{% endif %}
</ul>
</div>
</nav>
... lines 55 - 72
</body>
</html>

Ah! Let's go see it! Refresh! Our user drop-down is back! Oh, except all of these links go... nowhere. We can fix that!

For profile, that route is app_account: path('app_account'):

<!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">
{% if is_granted('ROLE_USER') %}
<li class="nav-item dropdown" style="margin-right: 75px;">
... lines 38 - 40
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{{ path('app_account') }}">Profile</a>
... lines 43 - 44
</div>
</li>
... lines 47 - 50
{% endif %}
</ul>
</div>
</nav>
... lines 55 - 72
</body>
</html>

For logout, that's path('app_logout'):

<!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">
{% if is_granted('ROLE_USER') %}
<li class="nav-item dropdown" style="margin-right: 75px;">
... lines 38 - 40
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
... lines 42 - 43
<a class="dropdown-item" href="{{ path('app_logout') }}">Logout</a>
</div>
</li>
... lines 47 - 50
{% endif %}
</ul>
</div>
</nav>
... lines 55 - 72
</body>
</html>

And, for "Create Post", we haven't built that yet. But, there is a controller called ArticleAdminController and we have at least started this. Give this route a name="admin_article_new":

... lines 1 - 14
class ArticleAdminController extends AbstractController
{
/**
* @Route("/admin/article/new", name="admin_article_new")
*/
public function new(EntityManagerInterface $em)
{
... lines 22 - 28
}
}

Tip

Oh! And don't forget to require ROLE_ADMIN on the controller!

... lines 1 - 6
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
... lines 8 - 11
/**
* @IsGranted("ROLE_ADMIN")
*/
class ArticleAdminController extends AbstractController
{
... lines 17 - 29
}

We'll link here, even though it's not done:

<!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">
{% if is_granted('ROLE_USER') %}
<li class="nav-item dropdown" style="margin-right: 75px;">
... lines 38 - 40
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
... lines 42 - 43
<a class="dropdown-item" href="{{ path('admin_article_new') }}">Create Post</a>
... lines 45 - 46
</div>
</li>
... lines 49 - 52
{% endif %}
</ul>
</div>
</nav>
... lines 57 - 74
</body>
</html>

Oh, but this link is only for admin users. So, surround this with is_granted("ROLE_ADMIN"):

<!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">
{% if is_granted('ROLE_USER') %}
<li class="nav-item dropdown" style="margin-right: 75px;">
... lines 38 - 40
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
... line 42
{% if is_granted('ROLE_ADMIN') %}
<a class="dropdown-item" href="{{ path('admin_article_new') }}">Create Post</a>
{% endif %}
... line 46
</div>
</li>
... lines 49 - 52
{% endif %}
</ul>
</div>
</nav>
... lines 57 - 74
</body>
</html>

Nice! Let's make sure we didn't mess up - refresh! Woohoo! Because we are logged in as an admin user, we see the user drop-down and the Create Post link.

Next: we need to talk about a few unique roles that start with IS_AUTHENTICATED and how these can be used in access_control to easily require login for every page on your site.

Leave a comment!

19
Login or Register to join the conversation
Farshad Avatar
Farshad Avatar Farshad | posted 1 year ago

Just want to give a compliment. These tutorials are great. However, I have a general questions. The Model View Controller (MVC model) is a known way in the programming world. I am wondering why is Model called Entity in Symfony? Why is View called Template? Is this done on purpose, because there is an underlying difference? Or is it the same principle?

Reply

Hey Farry7,

Basically, it's the same principle. But those terms are coming from Doctrine that is standalone library, Symfony just has integration for it. And also because entity is a bit more complex actually as it's used for ORM. About templates, well, view is a concept, but actually template is something that implements that concept I think. Anyway, template is much more understandable for more people than view I think. And it also comes from Twig template engine, it is called so (template engine), so it makes sense it works with templates :)

P.S. Btw, Symfony officially is not an MVC framework but Request-Response framework ;)

I hope this helps :)

Cheers!

Reply
Dung L. Avatar
Dung L. Avatar Dung L. | posted 2 years ago

Hello Symfonycasts,

Instead of coding for a dropdown menu, I am wondering if you know there is a bundle for it out there? I have searched but could not find one?

Cheers!

Reply

Hey Dung,

Nope, unfortunately, I don't know any bundles for this. But actually we already use a library that implements this dropdown :) Twitter bootstrap is already handle a lot of useful code behind the scen that we can re-use. As developers, we just need to put proper HTML code with CSS classes to make it working and that's it. In case you want to make it even simplier - I'd recommend you to look into direction of Twig macros for example. you can add a few dropdowns, notice some code duplication and then refactor this code with Twig macros. Then just call those macros passing proper argument and macro will build the dropdown for you. But just avoid to overcomplecate things. Sometimes it's easier to just copy/paste some ready-to-use template from the Twitter Boostrap's docs page than overload your macros with many arguments. So, if you need one drowdown, creating a macro for this purpose is just an overkill as for me, easier to copy/paste the ready-to-use code from the docs :)

I hope this helps!

Cheers!

Reply
Dung L. Avatar
Dung L. Avatar Dung L. | victor | posted 2 years ago | edited

Hi victor
1) I actually ended up with just using standard Boostrap drop down html :) - they are clean and short
2) I appreciate you introduce Twig macros, I have seen it somewhere but never known what it is but now I do and I will learn it :)
Thanks for your insight.
Dung.

Reply

Hey Dung,

Yeah, probably the best solution! About macros, we have a Twig course, and we also talk about macros there as well, you may want to check it: https://symfonycasts.com/sc...

Glad it helped!

Cheers!

1 Reply
Dung L. Avatar

Thank you!

Reply
Zesty Avatar

Hi Guys, I'm getting an error when trying to run this - PHP Fatal error: During class fetch: Uncaught ReflectionException: Class Symfony\Component\Form\FormTypeGuesserInterface not found in /vendor/symfony/doctrine-bridge/Form/DoctrineOrmTypeGuesser.php:25. Can anyone provide some help?

Reply

Hey Zesty

That's due a BC break between Symfony and latest PHP version. Here you can read more details about it: https://symfonycasts.com/sc...

Cheers!

Reply
unionelein Avatar
unionelein Avatar unionelein | posted 3 years ago

How can I refresh user role after I add it?
For example I added new role to logged in user, and then redirect to page that requires this new role. For now i get access denied.

Reply

Hey unionelein

At the moment there isn't any built-in feature for it, but here is a PR where you can find some workarounds: https://github.com/symfony/...

Cheers!

Reply
Remus M. Avatar
Remus M. Avatar Remus M. | posted 3 years ago

How do i setup a form to give User ChoiceType option to choose his role
This doesn't seem to work. its still seen as a string, not array ( upon submit )
( i know that i should set multiple to true because it's an array ) but i want the user to be able to choose only one role )


->add('roles', ChoiceType::class, array(
'choices' => ['Admin' => 'ROLE_ADMIN', 'User' => 'ROLE_USER', 'ROLE_CUSTOMER' => 'ROLE_CUSTOMER'],
'expanded' => true,
'multiple' => false,
'label' =>'x',
))

Reply

Hey Remus M.

That's weird, you should see a list of radio buttons because you activated the 'expanded' option. Can you show me a screenshot of how the form looks like? Also, give it a check to the html generated by the Form component, probably there might be a hint of what's going on.

Cheers!

Reply
Remus M. Avatar

registration form

upon submit ->

Expected argument of type "array", "string" given at property path "roles".

Reply

Ohhh of course! That's because your `roles` property expects an array and since you are selecting only one option you get in a string. I believe you will need a DataTransformer for this task.
You can find more information here: https://symfony.com/doc/cur...
Or you can watch how Ryan implement one here: https://symfonycasts.com/sc...

Cheers & Merry Christmas to you too (a bit late)!

1 Reply
Remus M. Avatar

I've come up with this so far


->add('roles',ChoiceType::class, [
'mapped' => false,
'expanded' => true,
'label' => 'Profile Type',
'choices' => [
'User' => self::RUSER,
'Agency' => self::RAGENCY,
],
'constraints' => [
new NotBlank ([
'message' => 'Choose a profile type !'
]),
]
])

it seems to work :)

Reply
Ad F. Avatar

I knew that it expects an array :) ( its more than obvious )

I don't know how to transform that field ...

Reply

Hey Ad F.!

This is an AWESOME question :), and you already understand the problem well. So, here are some options, depending on what you need:

1) The solution you posted above uses "mapped => false". That WILL work, but you will then need to read that data manually in the controller to set the string value from the ChoiceType, turn it into an array, and set on the object - e.g.


$user->setRoles(
[$form['roles']->getData()]
);

This is not a fancy solution, but it should work.

2) More generally, we have "roles" set as an array in the database, because Symfony expects getRoles() to return an array. But if you have a simpler setup where you only ever assign one role, you could simplify this:

A) Change the roles property in User to role and make it a string
B) Add getRole() and setRole() methods
C) In getRoles(), just return your one role, wrapped in an array (and maybe also with ROLE_USER)
D) In your form, the field would just be called "role"

Let me know if that makes sense!

Cheers!

1 Reply
Paul S. Avatar
Paul S. Avatar Paul S. | weaverryan | posted 3 years ago | edited

weaverryan

I'm not sure if you are still looking for this but here's my solution to the same problem of wanting the User Roles displayed as a single dropdown menu...

As Ryan ( weaverryan ) suggested I added a Data Transformer to my FormType class

Building my Admin Roles dropdown menu


$builder->
->add('roles', ChoiceType::class, [
'multiple' => false,
'label' => 'Set Account Role',
'choices' => [
'Please Select' => '',
'User' => 'ROLE_USER',
'Admin' => 'ROLE_ADMIN',
'Super Admin' => 'ROLE_SUPER_ADMIN',
],
]);

And then the Data Transformer itself...



$builder->get('roles')
->addModelTransformer(new CallbackTransformer(
function ($rolesArray){
if ($rolesArray) {
return count($rolesArray) ? $rolesArray[0] : null;
}
},
function ($rolesString){
return [$rolesString];
}));

Worked a treat and no unmapping from the User entity needed.

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