If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeEarlier, we talked about how the moment a user logs in, Symfony calls the getRoles()
method on the User
object to figure out which roles that user will have:
... lines 1 - 12 | |
class User implements UserInterface, PasswordAuthenticatedUserInterface | |
{ | |
... lines 15 - 26 | |
/** | |
* @ORM\Column(type="json") | |
*/ | |
private $roles = []; | |
... lines 31 - 78 | |
/** | |
* @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 90 - 154 | |
} |
This method reads a $roles
array property that's stored in the database as JSON... then always adds ROLE_USER
to it.
Until now, we haven't given any users any extra roles in the database... so all users have just ROLE_USER
. You can see this in the web debug toolbar: click to jump into the profiler. Yup, we have ROLE_USER
.
This is too boring... so let's add some true admin users! First, open config/packages/security.yaml
... and, down under access_control
, change this to once again require ROLE_ADMIN
:
security: | |
... lines 2 - 50 | |
access_control: | |
- { path: ^/admin, roles: ROLE_ADMIN } | |
... lines 53 - 54 |
Remember: roles are just strings that we invent... they can be anything: ROLE_USER
ROLE_ADMIN
, ROLE_PUPPY
, ROLE_ROLLERCOASTER
... whatever. The only rule is that they must start with ROLE_
. Thanks to this, if we go to /admin
... access denied!
Let's add some admin users to the database. Open up the fixtures class: src/DataFixtures/AppFixtures.php
. Let's see... down here, we're creating one custom user and then 10 random users. Make this first user an admin: set roles
to an array with ROLE_ADMIN
:
... lines 1 - 15 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
... lines 20 - 47 | |
UserFactory::createOne([ | |
'email' => 'abraca_admin@example.com', | |
'roles' => ['ROLE_ADMIN'] | |
]); | |
... lines 52 - 57 | |
} | |
} |
Let's also create one normal user that we can use to log in. Copy the UserFactory
code, paste, use abraca_user@example.com
... and leave roles
empty:
... lines 1 - 15 | |
class AppFixtures extends Fixture | |
{ | |
public function load(ObjectManager $manager) | |
{ | |
... lines 20 - 47 | |
UserFactory::createOne([ | |
'email' => 'abraca_admin@example.com', | |
'roles' => ['ROLE_ADMIN'] | |
]); | |
UserFactory::createOne([ | |
'email' => 'abraca_user@example.com', | |
]); | |
... lines 55 - 57 | |
} | |
} |
Let's do it! At your terminal, run:
symfony console doctrine:fixtures:load
When that finishes... spin over and refresh. We got logged out! That's because, when the user was loaded from the session, our user provider tried to refresh the user from the database... but the old user with its old id was gone thanks to the fixtures. Log back in.... with password tada
and... access granted! We rock! And in the profiler, we have the two roles.
In addition to checking or enforcing roles via access_control
... or from inside a controller, we often also need to check roles in Twig. For example, if the current user has ROLE_ADMIN
, let's a link to the admin page.
Open templates/base.html.twig
. Right after this answers link... so let me search for "answers"... there we go, add if, then use a special is_granted()
function to check to see if the user has ROLE_ADMIN
:
<html> | |
... lines 3 - 14 | |
<body> | |
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1"> | |
<div class="container-fluid"> | |
... lines 18 - 26 | |
<div class="collapse navbar-collapse" id="navbar-collapsable"> | |
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> | |
... lines 29 - 31 | |
{% if is_granted('ROLE_ADMIN') %} | |
... lines 33 - 35 | |
{% endif %} | |
</ul> | |
... lines 38 - 40 | |
</div> | |
</div> | |
</nav> | |
... lines 44 - 48 | |
</body> | |
</html> |
It's that easy! If that's true, copy the nav link up here... paste.. send the user to admin_dashboard
and say "Admin":
<html> | |
... lines 3 - 14 | |
<body> | |
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1"> | |
<div class="container-fluid"> | |
... lines 18 - 26 | |
<div class="collapse navbar-collapse" id="navbar-collapsable"> | |
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> | |
... lines 29 - 31 | |
{% if is_granted('ROLE_ADMIN') %} | |
<li class="nav-item"> | |
<a class="nav-link" href="{{ path('admin_dashboard') }}">Admin</a> | |
</li> | |
{% endif %} | |
</ul> | |
... lines 38 - 40 | |
</div> | |
</div> | |
</nav> | |
... lines 44 - 48 | |
</body> | |
</html> |
When we refresh... got it!
Let's do the same with the "log in" and "sign up" links: we only need those if we are not logged in. Down here, to simply check if the user is logged in, use is_granted('ROLE_USER')
... because, in our app, every user has at least that role. Add else
, endif
, then I'll indent. If we are logged in, we can paste to add a "Log out" link that points to the app_logout
route:
<html> | |
... lines 3 - 14 | |
<body> | |
<nav class="navbar navbar-expand-lg navbar-light bg-light px-1"> | |
<div class="container-fluid"> | |
... lines 18 - 26 | |
<div class="collapse navbar-collapse" id="navbar-collapsable"> | |
... lines 28 - 38 | |
{% if is_granted('ROLE_USER') %} | |
<a class="nav-link text-black-50" href="{{ path('app_logout') }}">Log Out</a> | |
{% else %} | |
<a class="nav-link text-black-50" href="{{ path('app_login') }}">Log In</a> | |
<a href="#" class="btn btn-dark">Sign up</a> | |
{% endif %} | |
</div> | |
</div> | |
</nav> | |
... lines 48 - 52 | |
</body> | |
</html> |
Cool! Refresh and... so much better. This is looking like a real site!
Next, let's learn about a few special "strings" that you can use with authorization: strings that do not start with ROLE_
. We'll use one of these to show how we could easily deny access to every page in a section except for one.
Hi @WilcoS!
That's a good question :). If you have some sort of "user admin" section. you could create a "checkbox" field for this. In Symfony forms, this would be something like this (in a form class):
$builder->add('roles', ChoiceType::class, [
'expanded' => true,
'multiple' => true,
'choices' => [
'Super Admin' => 'ROLE_SUPER_ADMIN',
'Editor' => 'ROLE_EDITOR',
],
])
Let me know if that helps!
Hi Ryan,
I have this in my formType but the select pulldown i getting the next error: Expected argument of type "array", "string" given at property path "roles".
How do I solve this?
$builder
->add('roles', ChoiceType::class, [
'choices' => [
'Gebruiker' => 'ROLE_USER',
'Administrator' => 'ROLE_ADMIN',
'Super admin' => 'ROLE_SUPER_ADMIN',
],
'multiple' => false,
'expanded' => false,
])
;
Thnx
Hey Wilco!
Apologies for the glacial reply - I had some vacation last week :).
Ok, so the problem here is 'multiple' => false,
and the fact that the roles
property on your User
object is technically an array. When you have multiple
false, it means that the user is selecting just one, string value. But then the form component looks at the User.roles
property and thinks "But hmm, I can't set this property to a string, it is an array!".
I understand why you have 'multiple' => false,
, however: you only want the user to worry about selecting a single role. The easiest way to solve this is:
You could simplify the roles
property on User
to be called role
and have this be a simple string. Then you would have normal getRole(): ?string
and setRole(string $role)
methods and you would call your form field role
(with no s
). You will STILL need a getRoles()
method, because the security system needs this. But it will now look something like this:
public function getRoles(): array
{
$roles = [$this->role];
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
The only reason this would NOT work is if, for some reason, you have some situation where a few users DO need multiple roles in the database. But it doesn't sound like you have that case.
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^3.3", // v3.3.0
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"doctrine/annotations": "^1.0", // 1.13.2
"doctrine/doctrine-bundle": "^2.1", // 2.6.3
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
"doctrine/orm": "^2.7", // 2.10.1
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"knplabs/knp-time-bundle": "^1.11", // v1.16.1
"pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
"pagerfanta/twig": "^3.3", // v3.3.0
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"scheb/2fa-bundle": "^5.12", // v5.12.1
"scheb/2fa-qr-code": "^5.12", // v5.12.1
"scheb/2fa-totp": "^5.12", // v5.12.1
"sensio/framework-extra-bundle": "^6.0", // v6.2.0
"stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
"symfony/asset": "5.3.*", // v5.3.4
"symfony/console": "5.3.*", // v5.3.7
"symfony/dotenv": "5.3.*", // v5.3.8
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/form": "5.3.*", // v5.3.8
"symfony/framework-bundle": "5.3.*", // v5.3.8
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/property-access": "5.3.*", // v5.3.8
"symfony/property-info": "5.3.*", // v5.3.8
"symfony/rate-limiter": "5.3.*", // v5.3.4
"symfony/runtime": "5.3.*", // v5.3.4
"symfony/security-bundle": "5.3.*", // v5.3.8
"symfony/serializer": "5.3.*", // v5.3.8
"symfony/stopwatch": "5.3.*", // v5.3.4
"symfony/twig-bundle": "5.3.*", // v5.3.4
"symfony/ux-chartjs": "^1.3", // v1.3.0
"symfony/validator": "5.3.*", // v5.3.8
"symfony/webpack-encore-bundle": "^1.7", // v1.12.0
"symfony/yaml": "5.3.*", // v5.3.6
"symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
"twig/extra-bundle": "^2.12|^3.0", // v3.3.3
"twig/string-extra": "^3.3", // v3.3.3
"twig/twig": "^2.12|^3.0" // v3.3.3
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
"symfony/debug-bundle": "5.3.*", // v5.3.4
"symfony/maker-bundle": "^1.15", // v1.34.0
"symfony/var-dumper": "5.3.*", // v5.3.8
"symfony/web-profiler-bundle": "5.3.*", // v5.3.8
"zenstruck/foundry": "^1.1" // v1.13.3
}
}
Hello,
When i edit a user with a form, how do i save then the user role to the database. It needs to be a array.
With the dataFixtures it works fine, but how do i this by a form. I mis this in the video's
Thnx