Buy
Buy

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%[email protected]:

... 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%[email protected]', $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%[email protected]', $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: [email protected], 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!

  • 2019-04-08 cybernet2u

    i'll try :) thanks

  • 2019-03-27 Paul Steven

    cybernet2u

    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.

  • 2019-03-01 Diego Aguiar

    Hey Алексей Суворов

    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!

  • 2019-02-28 Алексей Суворов

    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.

  • 2018-12-31 weaverryan

    Hey cybernet2u!

    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!

  • 2018-12-29 cybernet2u

    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 :)

  • 2018-12-28 cybernet2u

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

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

  • 2018-12-26 Diego Aguiar

    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)!

  • 2018-12-26 cybernet2u

    Marry xMas, btw :)

  • 2018-12-25 cybernet2u

    registration form

    upon submit ->

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

  • 2018-12-24 Diego Aguiar

    Hey cybernet2u

    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!

  • 2018-12-24 cybernet2u

    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',
    ))