Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Roles dinámicos

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

Antes hemos hablado de cómo, en el momento en que un usuario se conecta, Symfony llama al método getRoles()en el objeto User para averiguar qué roles tendrá ese usuario:

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

Este método lee una propiedad de la matriz $roles que está almacenada en la base de datos como JSON... y luego añade siempre ROLE_USER a la misma.

Hasta ahora, no hemos dado a ningún usuario ningún papel adicional en la base de datos... así que todos los usuarios tienen sólo ROLE_USER. Puedes ver esto en la barra de herramientas de depuración de la web: haz clic para saltar al perfilador. Sí, tenemos ROLE_USER.

Esto es demasiado aburrido... ¡así que vamos a añadir algunos verdaderos usuarios administradores! Primero, abreconfig/packages/security.yaml... y, debajo de access_control, cambia esto para requerir de nuevo ROLE_ADMIN:

security:
... lines 2 - 50
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
... lines 53 - 54

Recuerda: los roles son sólo cadenas que inventamos... pueden ser cualquier cosa: ROLE_USER ROLE_ADMIN ROLE_PUPPY, ROLE_ROLLERCOASTER... lo que sea. La única regla es que deben empezar por ROLE_. Gracias a esto, si vamos a /admin... ¡acceso denegado!

Rellenar los roles en la base de datos

Vamos a añadir algunos usuarios administradores a la base de datos. Abre la clase de accesorios:src/DataFixtures/AppFixtures.php. Veamos... aquí abajo, vamos a crear un usuario personalizado y luego 10 usuarios aleatorios. Haz que este primer usuario sea un administrador: ponroles en una matriz con 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
}
}

Vamos a crear también un usuario normal que podamos utilizar para iniciar la sesión. Copia el código de UserFactory, pégalo, usa abraca_user@example.com... y deja roles vacío:

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

¡Hagámoslo! En tu terminal, ejecuta:

symfony console doctrine:fixtures:load

Cuando termine... gira y actualiza. ¡Hemos cerrado la sesión! Eso es porque, al cargar el usuario desde la sesión, nuestro proveedor de usuarios intentó refrescar el usuario desde la base de datos... pero el antiguo usuario con su antiguo id había desaparecido gracias a las fijaciones. Vuelve a entrar en .... con la contraseña tada y... ¡acceso concedido! ¡Estamos de enhorabuena! Y en el perfilador, tenemos los dos roles.

Comprobación del acceso dentro de Twig

Además de comprobar o imponer los roles a través de access_control... o desde dentro de un controlador, a menudo también necesitamos comprobar los roles en Twig. Por ejemplo, si el usuario actual tiene ROLE_ADMIN, pongamos un enlace a la página del administrador.

Abre templates/base.html.twig. Justo después de este enlace de respuestas... así que déjame buscar "respuestas"... ahí vamos, añade if, y luego utiliza una función especial de is_granted() para comprobar si el usuario tiene ROLE_ADMIN:

<!DOCTYPE html>
<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>

¡Es así de fácil! Si es así, copia el enlace nav aquí arriba... pega... envía al usuario a admin_dashboard y di "Admin":

<!DOCTYPE html>
<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>

Cuando refresquemos... ¡ya está!

Hagamos lo mismo con los enlaces "iniciar sesión" y "registrarse": sólo los necesitamos si no estamos conectados. Aquí abajo, para comprobar simplemente si el usuario está conectado, utilizais_granted('ROLE_USER')... porque, en nuestra aplicación, todos los usuarios tienen al menos ese rol. Añade else, endif, y luego haré una sangría. Si hemos iniciado la sesión, podemos pegar para añadir un enlace "Cerrar sesión" que apunte a la ruta app_logout:

<!DOCTYPE html>
<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>

¡Genial! Refresca y... mucho mejor. ¡Esto parece un sitio real!

A continuación, vamos a conocer unas cuantas "cadenas" especiales que puedes utilizar con la autorización: cadenas que no empiezan por ROLE_. Utilizaremos una de ellas para mostrar cómo podemos denegar fácilmente el acceso a todas las páginas de una sección excepto a una.

Leave a comment!

5
Login or Register to join the conversation

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

Reply

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!

Reply

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

Reply

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!

1 Reply

Thnx it's working fine now.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

¡Este tutorial también funciona muy bien para Symfony 6!

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice