Symfony 4 - Define custom user roles from DB

user4112557 picture user4112557 · Feb 16, 2018 · Viewed 16.3k times · Source

I am learning Symfony 4, and I am lost with user roles and I don't know where to start. Because it will be an intranet like website Users will not register themselves, the admin will register them and set the role. The admin can also create new roles dynamically. I would like to store user roles in database. I have 2 entities User and Role. (Relations are not defined yet, that´s normal)

Here is my User entity :

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 * @UniqueEntity(fields="email", message="Email already taken")
 * @UniqueEntity(fields="username", message="Username already taken")
 */
class User implements UserInterface, \Serializable
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer", unique=true)
     */
    private $id;

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $firstname;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $lastname;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $email;

    /**
     * @ORM\Column(type="date")
     */
    private $birthdate;

    /**
     * @ORM\Column(type="integer", nullable=true)
     */
    private $roleId;

    /**
     * @ORM\Column(type="string", length=255, nullable=false)
     */
    private $username;

    /**
     * @Assert\NotBlank()
     * @Assert\Length(max=4096)
     */
    private $plainPassword;

    /**
     * @ORM\Column(type="string", length=255, nullable=false)
     */
    private $password;

    /**
     * @ORM\Column(name="is_active", type="boolean", nullable=true)
     */
    private $isActive;

    // GETTERS

    public function getId()
    {
        return $this->id;
    }

    public function getFirstname()
    {
        return $this->firstname;
    }

    public function getLastname()
    {
        return $this->lastname;
    }

    public function getBirthdate()
    {
        return $this->birthdate;
    }

    public function getEmail()
    {
        return $this->email;
    }

    public function getRoleId()
    {
        return $this->roleId;
    }

    public function getRoles()
    {
        return array('ROLE_USER');
    }

    public function getUsername()
    {
      return $this->username;
    }

    public function getPlainPassword()
    {
        return $this->plainPassword;
    }

    public function getPassword()
    {
      return $this->password;
    }

    public function getSalt()
    {
     return null;
    }

    // SETTERS

    public function setFirstname($firstname)
    {
        $this->firstname = $firstname;
    }

    public function setLastname($lastname)
    {
        $this->lastname = $lastname;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }

    public function setBirthdate($birthdate)
    {
        $this->birthdate = $birthdate;
    }

    public function setRoleId($roleId)
    {
        $this->roleId = $roleId;
    }

    public function setUsername($username)
    {
        $this->username = $username;
    }

    public function setPlainPassword($password)
    {
        $this->plainPassword = $password;
    }


    public function setPassword($password)
    {
        $this->password = $password;
    }

    // FUNCTIONS

    public function serialize()
    {
        return serialize(array(
            $this->id,
            $this->username,
            $this->password,
            // see section on salt below
            // $this->salt,
        ));
    }

    /** @see \Serializable::unserialize() */
    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->password,
            // see section on salt below
            // $this->salt
        ) = unserialize($serialized);
    }

    public function eraseCredentials()
    {

    }
}

Here is my Role entity :

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\RoleRepository")
 */
class Role
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer", unique=true)
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255, nullable=false)
     */
    private $type;

    // GETTERS

    public function getId()
    {
        return $this->id;
    }

    public function getType()
    {
        return $this->type;
    }

    // SETTERS

    public function setType($type): void
    {
        $this->type = $type;
    }
}

Is it a good practice to do so ??

On the Symfony documentation, we can read everywhere the "macros" ROLE_USER, ROLE_ADMIN... Where are they defined ? Can we customize this ?

Answer

dbrumann picture dbrumann · Feb 16, 2018

Symfony used to support role entities with a RoleInterface, much like with your user implementing the UserInterface. It was decided that this is unnecessary as the roles-table will usually only contain 2 fields (id, name) which means we might just as well store the roles directly in the users table.

So, if you want to follow Symfony best practices you would just have your roles in the user, for example like this:

class User implements UserInterface
{
    /** @Column(type="json") */
    private $roles = [];

    public function getRoles(): array
    {
        return array_unique(array_merge(['ROLE_USER'], $this->roles));
    }

    public function setRoles(array $roles)
    {
        $this->roles = $roles;
    }

    public function resetRoles()
    {
        $this->roles = [];
    }
}

If you don't want to store the roles as JSON-encoded string you can also use @Column(type="array") */ or write a custom DBAL type, e.g some kind of EnumType. You might also want to secure the setter against invalid data or add validation.

Regarding the second question about the roles used in the documentation: Symfony has some predefined roles and pseudo-roles for certain features:

  • IS_AUTHENTICATED_ANONYMOUSLY
  • IS_AUTHENTICATED_REMEMBERED
  • IS_AUTHENTICATED_FULLY
  • ROLE_ALLOWED_TO_SWITCH (for user switching)

The other roles like ROLE_USER and ROLE_ADMIN are purely exemplary and you may use them as you see fit. You do not even have to start your own roles with a ROLE_ (although some security features rely on this convention and things will not work as expected for those roles unless you do some manual changes).