1
votes

Hello Silex (and Symfony) experts,

I need to implement a database authentification User/Role model via Doctrine /ORM.

This is my silex composer setup:

"require": {
    "silex/web-profiler": "^2.0",
    "monolog/monolog": "1.13.*",
    "symfony/twig-bridge": "^3.2",
    "symfony/monolog-bridge": "^3.2",
    "symfony/console": "^3.2",
    "symfony/yaml": "^3.2",
    "symfony/security-bundle": "^3.2",
    "doctrine/orm": "^2.5",
    "dflydev/doctrine-orm-service-provider": "^2.0",
    "symfony/form": "^3.2",
    "symfony/validator": "^3.2",
    "symfony/config": "^3.2",
    "symfony/doctrine-bridge": "^3.2",
    "doctrine/migrations": "^1.5"
},

Users can register. Registered users can login and logout. Non registered visitors have anonymous role.

The symfony profiler is working, so I can see the security status (authentification/authoriszation). I also track the apache logfile for php errors.

I started from here https://github.com/fredjuvaux/silex-orm-user-provider (User from db, roles as array) and tried to expand it to get user roles from database via doctrine many-to-many relation.

There are:

class MyUserController (different user actions like user,edit, register,... )
class MyUserManager implements UserProviderInterface (loadUserByUsername, ...)
class MyUserServiceProvider implements ServiceProviderInterface, ControllerProviderInterface,  BootableProviderInterface (controller routing and template setting)

The ORM entities are:

User:
/**
 * MyUser
 *
 * @Entity 
 * @Table(name="myuser")
 */
class MyUser implements UserInterface, \Serializable 
{
 ....

    /**
     * @ManyToMany(targetEntity="MyRole", inversedBy="users")
     *
     */
     private $roles;

...
     * Constructor.
     *
     * @param string $email
     */
    public function __construct($email)
    {
        $this->email = $email;
        $this->created = time();
        $this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36);
        $this->roles = new ArrayCollection();

    }

    ... 

      /**
     *
     * @return ArrayCollection list of the user's roles.
     */
    public function getRoles()
    {
      $result = $this->roles->toArray();   // throws error for login: 
      //  $result = $this->roles; // test   // thhrows error : null object
      dump($this->roles);
      //  $result = array("ROLE_USER", "ROLE_OTHER"); // static setting and 
works for login
      return $result;
    }
    ...
}


Roles (implements Roleinterface)
/**
 * MyRole
 * 
 * @Entity
 * @Table(name="myrole")
 */
class MyRole implements RoleInterface
{

     /**
     * @var string
     * @Column(name="role", type="string", length=20, unique=true)
     */
    private $role;

    /**
     * @ManyToMany(targetEntity="MyUser", mappedBy="roles")
     */
    private $users;

    ...
    /*
    *  methods for RoleInterface
    *  @return string|null A string representation of the role, or null
    */
    public function getRole()
    {
       $result = $this->role;
       return $result;
    }

}

When a user registers, he gets for that session the ROLE_USER role, authentification and authorisation are ok and a user is created in the database.

Then I can assign new roles ("role_test1", "role_test2") in the controller for the new user, the many-to-many table myuser_myrole is filled (myuser_id myrole_id). When I change the roles, they are correctly updated by the entity manager.

When I access the user Entity from the userController to work on it, I can access the assigned roles:

// MyUserController.php
 $user = $em->getRepository('MyEntities\MyUser')->find($id);
 $roles= $user->getRoles()
 $role_length = count($roles);
 $role_list = array();
 for ($i=0; $i <$role_length ; $i++) 
 { 
      array_push($role_list,$roles[$i]->getRole());    // MyRole::getRole() prints out something to screen.
 }
 printf("<br> role-list:"); dump($role_list);

Calling this controller prints out the assigned roles via MyRole::getRole(), so ORM access works here.

Now comes the strange:

I want to login the new user with the login form.

When I use

// MyUser::getRoles() 
  return $this->roles;

It throws:

Argument 4 passed to Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken::__construct() must be of the type array, object given, 

Ok, makes maybe sense because the $roles is an Doctrine ArrayCollection.

When I use

// MyUser::getRoles() 
  return $this->roles->toArray();

I can login with user password,but am not authenticated (yellow status). Dumping out the roles, I receive an empty array ArrayCollection.

roles:
ArrayCollection {#388 ▼
  -elements: []
}

The UsernamePasswordToken has an empty role-array.

When I use
// MyUser::getRoles() 
  return  array("ROLE_HELLO1", "ROLE_HELLO2");  // static role array with strings

I can login and am authenticated with these roles:

Roles

array:2 [▼
  0 => "ROLE_HELLO1"
  1 => "ROLE_HELLO2"
]

There are old docs about this (Managing Roles in the Database) for symfony 2 http://symfony.com/doc/2.0/cookbook/security/entity_provider.html, but it doesnt work in symfony3.

Here they use

   //class User
   public function getRoles()
    {
        return $this->groups->toArray();
    }

    //class Group extends Role (not RoleInterface, old?)
    public function getRole()
    {
        return $this->role;
    }

The actual symfony docs for user management do not show how to use roles stored in database.

In summary:

Login and user/role do not work as expected:

MyUser::getRoles() 
  • does not receive the Roles from database via doctrine ORM.

  • has to return a string array of roles for login.

  • delivers the correct role association in another controller.

Questions:

(1) Is this a Silex specific issue?

(2) How to use it correctly or where is a good link/doc for a workaround?

(3) Does the method LoadUserByUsername() interfere with all this?

(4) Do I need a class MyUserRepository extends EntityRepository {} to do the query and get the Role List?

(5) Do I need to use the Role Hierarchy service?

(6) Are there special naming conventions(tablename or class name) for "user" and "role"?

I found many posts asking the same/similar but they do not help here.

Thank you for help, I am really stuck on that!

dirk

2
There is an up to date article about loading users from Database - mTorres

2 Answers

0
votes

Try this:

public function getRoles()
{
    return $this->roles->map(function (MyRole $role) {
        return $role->getRole();
    })->toArray();
}

You should also check if the relationship is correctly saved in database. If there is ManyToMany relationship between MyUser and MyRole, you have to ensure that relationship is saved in both entities.

//class MyUser
public function addRole(MyRole $role)
{
    $this-roles->add($role);
    $role->users->add($user);
}
0
votes

I had a break on this, but now it seems to work. Thank you miikes for the addRole() suggestion!

finally I have: MyUser.php:

//Myuser.php
/**
 * MyUser
 *
 * @Entity 
 * @Table(name="myuser")
 */

class MyUser implements UserInterface,  \Serializable //, ObjectManagerAware
{
    ...

    /**
     * @ManyToMany(targetEntity="MyRole", inversedBy="users")
     */
    private $roles;

    public function __construct($email)
    {
    (...)
    $this->roles = new ArrayCollection();


     /**
     *
     * @return ArrayCollection list of the user's roles.
     */
    public function getRoles()
    {   
       $result = $this->roles->toArray();
       return $result;
    }

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

    public function setRole($myrole)
    {   
    $this->roles= $myrole;
    }

    public function hasRole($role)
    {
        return in_array(strtoupper($role), $this->getRoles(), true);
    }

        public function addRole(MyRole $role)
    {
      $this->roles->add($role);
      //$role->users->addRole($this); // could not access roles->user->...  
                      // because private variable in MyRole but it works 
    }

        /**
     * Remove the given role from the user.
     *
     * @param string $role
     */
    public function removeRole($role)
    {   
      dump($role);
      $this->roles->removeElement($role);
    }

    (...) // other setters getters

    public function serialize()
    {
        return serialize(array(
            $this->id,
            $this->username,
            $this->password,
            $this->salt, 
        ));
    }

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

and MyRole.php:

// MyRole.php

/**
 * MyRole
 * 
 * @Entity
 * @Table(name="myrole")
 */
class MyRole implements RoleInterface
{
    (...)

    /**
     * @ManyToMany(targetEntity="MyUser", mappedBy="roles")
     */
    private $users;


     /**
     * @var string
     * @Column(name="role", type="string", length=20, unique=true)
     */
    private $role;


    /*
    *  methods for RoleInterface
    *  @return string|null A string representation of the role, or null
    */
    public function getRole()
    {
       $result = $this->role;
       return ($result);
    }

    public function setRole($role)
    {
        $this->role= $role;
        return $this;
    }

    (...)

     /**
     * Constructor
     */
    public function __construct()
    {
        $this->users = new ArrayCollection();
    }

     /**
     * Add user
     * @param \MyEntities\MyUser $user
     * @return MyRole
     */
    public function addUser($user)
    {
        $this->users[] = $user;
        return $this;
    }


    public function setUser($user)
    {
        $this->users[] = $user;
        return $this;
    }

    /**
     * Remove user
     *
     * @param \MyEntities\MyUser $user
     */
    public function removeUser($user)
    {
        $this->users->removeElement($user);
    }

    /**
     * Get users
     *
     * @return ArrayCollection $users
     */
    public function getUsers()
    {
        return $this->users;
    }

     /**
     * __toString()
     *
     * @return string
     */
    public function __toString()
    {
        return $this->bezeichnung;
    }
}

With the help of the doctrine orm commands

vendor/bin/doctrine  orm:validate-schema
vendor/bin/doctrine  orm:schema-tool:update  --dump-sql

the correct manyToMany table myuser_myrole was generated and the role setting works at login of a user.

I think, the most important was the correct use of the function addRole() (with this->roles->add($role), and not something like this->roles->addRole($role) ) to let doctrine do the magic stuff in the background.

Thanks for any help and comments! dirk