Symfony 4 Doctrine, How to load user Roles from the Database

10,308

Solution 1

So far, you haven't mapped your User to Roles as per your database structure.

private $roles;

Has no information about how it maps to the roles table. It should look something like:

/**
 * @var Collection|Role[]
 * @ORM\ManyToMany(targetEntity="Role")
 * @ORM\JoinTable(
 *      name="user_roles",
 *      joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")},
 *      inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")}
 * )
 */
private $roles;

You'll also need to create an initial set of roles in construct so that getRoles doesn't throw an error and so that roles can be added to new users one by one if needed:

public function __construct()
{
    $this->isActive = true;
    $this->roles = new ArrayCollection();
}

You can delete getRole() and setRole() because we don't have a single role (unless it's required by the interface), and you can lose the current $role property:

/**
 * @ORM\Column(type="string", length=254, nullable=true)
 */
private $role;

but add a setter that takes a collection:

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

Then to get the Roles:

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

If you're using a form to create user (especially a Sonata Admin one), you may use the following methods in addition to add and remove single Roles from a User (it will remove the relationship between the user and the role, not the role itself):

public function addRole(Role $role)
{
    $this->roles->add($role);
}

And one to remove a role:

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

Solution 2

The scenario of having 3 tables (users / roles / user_roles) is so common that it should be documented in the manuals.

In my case, to make it work, I applied the answer of "OK sure", then I hit the problem signaled by "Vasiliy Toporov" and "Radu". $this->roles->toArray() is not enough in getRoles(), because it returns an array of Role entity, instead of the expected array of strings (expected by Symfony\Component\Security\Core\Authentication\Token\AbstractToken).

To make it work, I first added in the Role entity class (rlCode = the string code; ROLE_ADMIN etc):

public function __toString(): string
{
    return $this->rlCode;
}

Then, in the User entity class I changed getRoles() to:

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

    foreach($this->roles->getIterator() as $i => $item) {
        $arrRolesString[] = (string)$item;
    }
    return $arrRolesString;        
}

Now it works. The next problem I am having, however, is that all multiple roles assigned to an user are a duplicate of the first role retrieved by the join query, and I have no idea why (the join query returns all roles correctly, but it must be a problem in the ManyToMany assignation somewhere...if anyone knows, please tell)

EDIT: please ignore the duplicate issue. It was because Doctrine make entity maps tinyint (my id column in the Roles table is tinyint) as boolean, instead of integer.

Share:
10,308
Sanjok Gurung
Author by

Sanjok Gurung

ISHTUDENT

Updated on June 04, 2022

Comments

  • Sanjok Gurung
    Sanjok Gurung almost 2 years

    New to Symfony. How do I load the current loggedin user's role from the database using Doctrine. I have 3 tables laid out like so.

    users => (user_id, username, password, email)

    user_roles => (id,user_id,role_id)

    roles => (role_id, role_name)

    I have entities and their corresponding repositories for each table.

    My security.yaml looks like this.

    security:
    encoders:
        App\Entity\User:
            algorithm: bcrypt
    
    providers:
        our_db_provider:
            entity:
                class: App\Entity\User
                property: username
                # if you're using multiple entity managers
                # manager_name: customer
    
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
    #            pattern: ^/
    #            http_basic: ~
            provider: our_db_provider
            anonymous: true
            form_login:
                #login_path is GET request used ti display the form
                # needs to be route names(alias) not the path.
                login_path: login
    
                #check_path is a POST request
                check_path: logincheck
                use_forward: true
    
                default_target_path: default
                always_use_default_target_path: true
    

    My Entity/User implements UserInterface component and by reading documents I came to know that the getRoles() method is responsible for updating user roles. I have created a custom method called getUserRoles($id) in my UserRolesRepository.php where I managed to return string array of the current user's roles however I am not able to access this method from the Entity. I know I should not access Repository methods from an Entity class, but I am dearly stuck at this stage. So for now my getRoles() method returns static array return array('ROLE_ADMIN', 'ROLE_EDITOR');

    My User Entity Class

    namespace App\Entity;
    
    use App\Repository\UserRepository;
    use Doctrine\ORM\EntityManager;
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Security\Core\User\UserInterface;
    use App\Repository\UserRolesRepository;
    use Doctrine\ORM\EntityRepository;
    use App\Services\Helper;
    
    /**
     * @ORM\Table(name="`user`")
     * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
     */
    class User implements UserInterface, \Serializable
    {
        /**
        * @ORM\Id()
        * @ORM\GeneratedValue()
        * @ORM\Column(type="integer")
        */
    private $id;
    
    /**
     * @ORM\Column(type="string", length=25, nullable=true)
     */
    private $username;
    
    /**
     * @ORM\Column(type="string", length=64, nullable=true)
     */
    private $password;
    
    /**
     * @ORM\Column(type="string", length=254, nullable=true)
     */
    private $email;
    
    /**
     * @ORM\Column(type="boolean", nullable=true)
     */
    private $isActive;
    
    private $roles;
    
    /**
     * @ORM\Column(type="string", length=254, nullable=true)
     */
    private $role;
    
    public function __construct()
    {
        $this->isActive = true;
    }
    
    /**
     * @return mixed
     */
    public function getRole()
    {
        return $this->role;
    }
    
    /**
     * @param mixed $role
     */
    public function setRole($role)
    {
        $this->role = $role;
    }
    
    public function getId()
    {
        return $this->id;
    }
    
    public function getUsername(): ?string
    {
        return $this->username;
    }
    
    public function setUsername(?string $username): self
    {
        $this->username = $username;
    
        return $this;
    }
    
    public function getPassword(): ?string
    {
        return $this->password;
    }
    
    public function setPassword(?string $password): self
    {
        $this->password = $password;
    
        return $this;
    }
    
    public function getEmail(): ?string
    {
        return $this->email;
    }
    
    public function setEmail(?string $email): self
    {
        $this->email = $email;
    
        return $this;
    }
    
    public function getIsActive(): ?bool
    {
        return $this->isActive;
    }
    
    public function setIsActive(?bool $isActive): self
    {
        $this->isActive = $isActive;
    
        return $this;
    }
    
    
    //return is required or else returns an fatal error.
    public function getRoles()
    {
        return array('ROLE_ADMIN','ROLE_EDITOR');
    }
    
    public function eraseCredentials()
    {
        // TODO: Implement eraseCredentials() method.
    }
    
    public function serialize()
    {
        // TODO: Implement serialize() method.
        return serialize(array(
            $this->id,
            $this->username,
            $this->password,
        ));
    }
    
    /** @see \Serializable::unserialize() */
    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->password,
            // see section on salt below
            // $this->salt
            ) = unserialize($serialized, ['allowed_classes' => false]);
    }
    
    public function getSalt()
    {
        // TODO: Implement getSalt() method.
        return null;
    }
    }
    
  • Sanjok Gurung
    Sanjok Gurung about 6 years
    Thank you, I am actually only relying on Symfony's built in role system. The only problem I am having is that my roles are in a table and I need to access the currently logged in user's role/s from getRoles() in my User entity.
  • Sanjok Gurung
    Sanjok Gurung about 6 years
    Thank you for this brilliant answer, We have another external form using Idiorm to register users, which will populate the users,user_role,roles tables. I do not explicitly need to use addRole and removeRole do I?? I mean I can manage externally if I want to cant I??
  • OK sure
    OK sure about 6 years
    If you don't need them, then they can be removed for sure. So long as you're creating an ArrayCollection to put into the setRoles method from the form you should be fine: new ArrayCollection($rolesArray);
  • Sanjok Gurung
    Sanjok Gurung about 6 years
    Just to understand, if I were to revoke a role from a user removeRole(Role $role) would remove the actual role from the roles table wouldn't it. if I wanted to remove only the roles from the user, for instance in my case I would only like to remove role_id user_id from he user_roles table. Would removeRole achieve that??
  • OK sure
    OK sure about 6 years
    Removing the role using the removeRole method above wouldn't remove the actual role from the roles table but would remove the relationship between that user and the role.
  • Sanjok Gurung
    Sanjok Gurung about 6 years
    Excellent, Thank you.
  • Vasiliy Toporov
    Vasiliy Toporov over 5 years
    Custom Role entity should extends Symfony\Component\Security\Core\Role\Role because RoleInterface was deprecated and removed from Symfony 4. Without this extending you will get exception in Symfony\Component\Security\Core\Authentication\Token constructor.
  • Radu
    Radu about 5 years
    I think that returning $this->roles->toArray(); in getRoles() is not enough because it returns an array of objects which won't satisfy PostAuthenticationGuardToken::__constructor(). The 4th $roles parameter needs to be an array with string values.
  • Prawny
    Prawny almost 5 years
    Why is none of this even hinted in the documentation?