Buy Access to Course
16.

Dynamic Roles

Share this awesome video!

|

Keep on Learning!

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:

20 lines | src/Controller/AccountController.php
// ... 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:

20 lines | src/Controller/AccountController.php
// ... 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:

20 lines | src/Controller/AccountController.php
// ... 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:

37 lines | src/Controller/CommentAdminController.php
// ... 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"):

24 lines | src/Controller/AccountController.php
// ... 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:

130 lines | src/Entity/User.php
// ... 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:

50 lines | src/DataFixtures/UserFixture.php
// ... 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:

50 lines | src/DataFixtures/UserFixture.php
// ... 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:

50 lines | src/DataFixtures/UserFixture.php
// ... 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:

130 lines | src/Entity/User.php
// ... 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:

74 lines | templates/base.html.twig
<!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:

75 lines | templates/base.html.twig
<!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:

75 lines | templates/base.html.twig
<!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'):

75 lines | templates/base.html.twig
<!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'):

75 lines | templates/base.html.twig
<!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":

31 lines | src/Controller/ArticleAdminController.php
// ... 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!

31 lines | src/Controller/ArticleAdminController.php
// ... 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:

77 lines | templates/base.html.twig
<!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"):

77 lines | templates/base.html.twig
<!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.