Symfony: gestionando los roles de usuario

2020-12-19 - Categorías: PHP / Symfony
Symfony login for testing

Ampliando un post anterior sobre Symfony, traigo hoy el cómo gestionar los roles de usuario con un panel de control dentro de un proyecto. En este post anterior sobre la gestión de la seguridad con Symfony quedó en el aire este tema, así que vamos al grano con el codekata.. ?

Punto de partida

El punto de partida del post anterior es que tenemos un directorio protegido por usuario y contraseña llamado /protected/. Bajo el directorio principal / tenemos acceso anónimo, es decir público. Tenemos también un CRUD de los usuarios bajo el directorio /user/. Todos los usuarios que añadimos tienen el rol ROLE_USER asignado por programación.

Todo el código fuente de este punto de partida está aquí:
https://github.com/jaimenj/symfony-starter-tutorials/tree/master/symfony-tutorial-16

Añadiendo en el formulario la edición de los roles

Esta es la forma más sencilla, añadiendo en el mismo formulario los roles que queramos elegir para los usuarios. El formulario podría quedar así:

<?php

namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $entity = $builder->getData();

        $builder
            ->add('email')
            ->add('roles', ChoiceType::class, [
                'choices' => [
                    'User' => 'ROLE_USER',
                    'Admin' => 'ROLE_ADMIN',
                ],
                'expanded' => true,
                'multiple' => true,
                'data' => $entity->getRoles() // Current roles assigned..
            ])
            ->add('password')
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => User::class,
        ]);
    }
}

Editando la entidad User para que no fuerce el tener como mínimo el rol ROLE_USER podría quedar así:

    /**
     * @see UserInterface
     */
    public function getRoles(): array
    {
        /*$roles = $this->roles;
        // guarantee every user at least has ROLE_USER
        $roles[] = 'ROLE_USER';*/

        return array_unique($this->roles);
    }

Una configuración de Twig para que se muestre el formulario usando los estilos de formularios horizontales de Bootstrap, podría hacerse editando el fichero config/packages/twig.yaml dejando algo como lo siguiente:

twig:
    default_path: '%kernel.project_dir%/templates'
    form_themes: ['bootstrap_4_layout.html.twig']

Si todo ha ido bien, en local, al editar o añadir un usuario deberíamos de ver algo como lo siguiente:

Formulario en Symfony editando roles de usuario..

Como el formulario mapea el campo roles a la entidad no hay que hacer nada más. Symfony se encargará de guardar los roles.

Añadiendo una entidad para gestionar los roles de usuario

Otra forma de gestionar estos roles es tenerlos en una entidad aparte. Para esto haría falta dicha entidad, y su gestión con un CRUD. Para esto generamos dicha entidad con un comando de consola:

php bin/console make:entity Role

Le podríamos poner los siguientes campos:

  • code: para guardar el código de rol (ROLE_USER, ROLE_ADMIN, etc..).
  • description: una descripción.

Hacemos un CRUD:

php bin/console make:crud Role

Ahora toca actualizar la base de datos, refrescar la caché por si acaso algo falla, y probar:

php bin/console cache:clear
php bin/console doctrine:schema:update --dump-sql --force

Si todo ha ido bien ya tendremos bajo la ruta http://localhost:8000/role/ un clásico CRUD con para listar, añadir, editar y borrar roles.

Symfony, gestionando roles de usuario con un CRUD.

Enlazando las entidades User y Role

Lo siguiente es un poco tricky, porque tendremos que borrar de la entidad User el campo roles, para entonces enlazar User con Role en una relación ManyToMany. Es este tipo de relación porque un usuario podrá tener varios roles, y un role puede asignarse también a varios usuarios.

Para poder hacerlo bien tendremos que dejar de implementar en la clase User la interfaz de usuarios porque nos obliga a implementar dichas funciones. En la declaración de la clase:

class User //implements UserInterface
{

Ya podemos borrar $roles y sus funciones de User, para usar el maker de Symfony y enlazar más rápido las dos entidades. Lo dicho:

php bin/console make:entity User

Debería de quedarnos ahora la relación así en User:

    /**
     * @ORM\ManyToMany(targetEntity=Role::class, inversedBy="users")
     */
    private $roles;

    /**
     * @return Collection|Role[]
     */
    public function getRoles(): Collection
    {
        return $this->roles;
    }

    public function addRole(Role $role): self
    {
        if (!$this->roles->contains($role)) {
            $this->roles[] = $role;
        }

        return $this;
    }

    public function removeRole(Role $role): self
    {
        if ($this->roles->contains($role)) {
            $this->roles->removeElement($role);
        }

        return $this;
    }

En la clase Role quedará así:

    /**
     * @ORM\ManyToMany(targetEntity=User::class, mappedBy="roles")
     */
    private $users;

    /**
     * @return Collection|User[]
     */
    public function getUsers(): Collection
    {
        return $this->users;
    }

    public function addUser(User $user): self
    {
        if (!$this->users->contains($user)) {
            $this->users[] = $user;
            $user->addRole($this);
        }

        return $this;
    }

    public function removeUser(User $user): self
    {
        if ($this->users->contains($user)) {
            $this->users->removeElement($user);
            $user->removeRole($this);
        }

        return $this;
    }

Arreglos en las plantillas

Hace falta entonces para mostrar los roles asignados, recorrer el array de roles, asignados al usuario en curso. Por ejemplo para el fichero symfony-tutorial-16b/templates/user/index.html.twig tenemos lo siguiente:

                <td>
                    {% for role in user.roles %}
                        {{ role }}<br>
                    {% endfor %}
                </td>

Y para mostrar cada role hay añadir, a la entidad Role, en el fichero src/Entity/Role.php, la función pública __toString(), para poder mostrar como texto cada Role:

    public function __toString() {
        return $this->code.' '.$this->description;
    }

Todos los fuentes de este post están disponibles en:
https://github.com/jaimenj/symfony-starter-tutorials/tree/master/symfony-tutorial-16b

El formulario para editar los roles con la relación

Antes listaba los roles por programación en PHP, ahora los tiene que listar desde la entidad relacionada Role, con los valores que haya en la base de datos.

Tendremos el problema siguiendo este codekata, porque Symfony fuerza a que la función $user->getRoles() devuelva un array de cadenas, pero usando el maker anterior tendremos que getRoles() devuelve una colección de objetos, los objetos en $this->roles. Luego dejo una de las posibles soluciones.

El el fichero src/Entity/User.php necesitaremos entonces añadir o modificar estas dos funciones:

    public function getRoles()
    {
        $rolesArray = [];

        foreach ($this->roles as $role) {
            $rolesArray[] = $role->getCode();
        }

        return $rolesArray;
    }

    public function getRolesObjects() {
        return $this->roles;
    }

Es decir, que esto deja sin funcionar el guardado del formulario. Pero un posible arreglo sería hacer dos cosas, por un lado el formulario podría quedar así:

<?php

namespace App\Form;

use App\Entity\Role;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;

class UserType extends AbstractType
{
    private $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $entity = $builder->getData();

        $rolesChoices = [];
        $roles = $this->entityManager->getRepository(Role::class)->findAll();
        foreach ($roles as $rol) {
            $rolesChoices[$rol->__toString()] = $rol->getId();
        }
        $rolesIds = [];
        foreach ($entity->getRolesObjects() as $role) {
            $rolesIds[] = $role->getId();
        }

        $builder
            ->add('email')
            ->add('roles_in_form', ChoiceType::class, [
                'mapped' => false,
                'expanded' => true,
                'multiple' => true,
                'choices' => $rolesChoices,
                'data' => $rolesIds,
            ])
            ->add('password', null, ['data' => ''])
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => User::class,
        ]);
    }
}

Y por otro lado, para que podamos guardar los roles, tendríamos que asignar manualmente los roles al recibir el formulario, esto lo podemos hacer así:

    /**
     * @Route("/{id}/edit", name="user_edit", methods={"GET","POST"})
     */
    public function edit(Request $request, User $user, UserPasswordEncoderInterface $encoder): Response
    {
        $form = $this->createForm(UserType::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // Update password..
            $entityManager = $this->getDoctrine()->getManager();
            $passwordEncoded = $encoder->encodePassword($user, $user->getPassword());
            $user->setPassword($passwordEncoded);

            // Update roles..
            $arrayRoles = $form['roles_in_form']->getData();
            $roles = $entityManager->getRepository(Role::class)->findAll();
            foreach ($roles as $role) {
                if (in_array($role->getId(), $arrayRoles)) {
                    $user->addRole($role);
                } else {
                    $user->removeRole($role);
                }
            }

            $entityManager->persist($user);
            $entityManager->flush();

            return $this->redirectToRoute('user_index');
        }

        return $this->render('user/edit.html.twig', [
            'user' => $user,
            'form' => $form->createView(),
        ]);
    }

Si has seguido el codekata completo, o te has descargados los códigos fuentes, se debería de ver el nuevo formulario de edición de roles así:

Symfony, nueva edición de roles a un usuario.

Y el listado de usuarios podría quedar tal que así:

Symfony, listando usuarios y sus roles.

Entrando en la zona /protected/

Como he escrito arriba, para que funcione la autenticación y autorización de usuarios con Symfony, la función getRoles() tiene que devolver exactamente un array de strings con los roles del usuario en curso.

Esto lo podemos arreglar editando la entidad User con los cambios citados anteriormente en el fichero src/Entity/User.php, para que haga esto con esta función:

    public function getRoles()
    {
        $rolesArray = [];

        foreach ($this->roles as $role) {
            $rolesArray[] = $role->getCode();
        }

        return $rolesArray;
    }

En la documentación oficial hay un mecanismo alternativo:
https://symfony.com/doc/2.2/cookbook/security/entity_provider.html

Si todo va bien, se debería poder acceder al directorio /protected/ con un usuario y contraseña como en la imagen siguiente:

Symfony usuario logueado en el directorio /protected/.

Dejo todo el código fuente probado y funcionando en el repositorio:
https://github.com/jaimenj/symfony-starter-tutorials/tree/master/symfony-tutorial-16b

Si has llegado hasta aquí espero poder haberte ayudado, no dudes en dejar un comentario.. ¡Un saludo!

2 respuestas a “Symfony: gestionando los roles de usuario”

  1. Paco dice:

    Hola Jaime. Como ya sabes vengo siguiendo tus tutoriales paso a paso. Te voy a hacer una serie de comentarios por si alguno lo consideras interesante:

    1º Cuando creo la relacion ManyToMany en la entidad USer se me generan tres nuevas funciones. La primera de ellas es =>> public function getRole(): Collection Luego en tu codigo veo que se llama getRoles y más tarde para que funcione la tengo que renombrar por getRolesObjects.

    2º A la hora de modificar el código de algún archivo, sería interesante indicar dicho archivo (por ejemplo cuando hablas del «Arreglo en las plantillas»)

    3º Finalmente algo falla en la codificación de la contraseña (estoy investigándolo)

    Te hago esta serie de comentarios porque creo que para tí son útiles.

    Seguiré hasta el final tu magnífico trabajo y si así me lo indicas continuaré haciéndote observaciones.

    Muchas gracias por compartirlo.

    • Jnj dice:

      Buenos días Paco!

      Primero que todo, muchísimas gracias por dejar el comentario con los apuntes para mejorar el post ???
      Estoy revisando los fuentes citados, acabo de modificar el post con lo que comentas, vamos por puntos:

      1ª Modificado, en el fichero src/Entity/User.php harán falta las dos funciones getRoles() y getRolesObjects(). El getRole() en singular puede que lo tengas al usar el maker de Symfony, pero debería de haberse creado en prural.
      2º Modificado, añadidos paths y link a GitHub con todos los fuentes relacionados con el post.
      3º Con lo de la codificación, si tienes más información te lo agradecería. Sólo me he encontrado con ese problema en la codificación al pintar una contraseña en el navegador. Pero conviene no escribir nunca las contraseñas en ningún lado, ni en plantillas, ni emails, ni nada, por temas de seguridad.

      Por favor, no dejes de hacer observaciones de todo lo que consideres. Muchas gracias!

      Un saludo.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

 

© 2024 JnjSite.com - MIT license

Sitio hecho con WordPress, diseño y programación del tema por Jnj.