I have the following entity in Symfony :
class User implements AdvancedUserInterface, \Serializable {
...
private $roles;
...
public function __construct()
{
...
$this->roles = new ArrayCollection();
// Default role for evey user (new entity);
$this->roles->add("ROLE_USER");
...
}
...
function getRoles() {
return $this->roles->toArray();
}
...
function addRole($role){
$this->roles->add($role);
}
function removeRole($role){
$this->roles->remove($role);
}
...
public function serialize()
{
return serialize(array(
...
$this->roles,
...
));
}
/** @see \Serializable::unserialize() */
public function unserialize($serialized)
{
list (
...
$this->roles,
...
) = unserialize($serialized, ['allowed_classes' => false]);
}
}
When I register one user, the default role (ROLE_USER) is added correctly. But when I try to edit one, the database recors does not change :
public function UserAddRole(Request $request){
$userId = $request->request->get("userId");
$role = "ROLE_" . strtoupper($request->request->get("role"));
if($role == "ROLE_USER"){
throw $this->createNotFoundException(
'Cannot remove ROLE_USER role'
);
}
$user = $this->getDoctrine()
->getRepository(User::class)
->findOneBy(array(
'id' => $userId
));
if (!$user) {
throw $this->createNotFoundException(
'User not found'
);
}
$user->addRole($role);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
return new Response("<pre>".var_dump($user->getRoles())."</pre>");
}
There are no constraints on this field, only hardcoded values for now.
The array returned in the response contains the roles I want, but not the database (checked by reloading the page and directly in MySQL.
Any idea ?
Thanks
There are a few different things happening here. Per your comment above, you said you are mapping $roles
to the array
type. This is stored in the database by calling the native PHP functions serialize(...)
and unserialize(...)
. That means that if an object had roles of ROLE_USER
and ROLE_ADMIN
, the data would look like this:
a:2:{i:0;s:9:"ROLE_USER";i:1;s:10:"ROLE_ADMIN";}
When Doctrine loads your object, it will use the internal PHP array
type to store this data, meaning that $this->roles
would have a runtime value of array('ROLE_USER', 'ROLE_ADMIN')
in this example.
A similar type is simple_array
, which behaves the same inside your application, but stores the value as a comma-delimited list in the database. So in this case, your database data would just be:
ROLE_USER,ROLE_ADMIN
Currently in your constructor, you are using the Doctrine ArrayCollection
type to initialize $roles
as a collection. However, if the field is mapped as array
, after retrieving the object from the database, $roles
will be a PHP array
type, not an ArrayCollection
object. To illustrate the difference:
// the constructor is called; $roles is an ArrayCollection
$user = new User();
// the constructor is not called; $roles is an array
$user = $entityManager->getRepository(User::class)->findOneById($userId);
Generally speaking, and actually in every case I've ever run into, you only want to initialize to ArrayCollection
for association mappings, and use array
or simple_array
for scalar values, including the roles property.
You can still achieve your desired addRole(...)
and removeRole(...)
behavior by using a little bit of PHP. For example, using Doctrine annotation mapping:
use Doctrine\ORM\Mapping as ORM;
...
/**
* @ORM\Column(name="roles", type="simple_array", nullable=false)
*/
private $roles;
...
/**
* Add the given role to the set if it doesn't already exist.
*
* @param string $role
*/
public function addRole(string $role): void
{
if (!in_array($role, $this->roles)) {
$this->roles[] = $role;
}
}
/**
* Remove the given role from the set.
*
* @param string $role
*/
public function removeRole(string $role): void
{
$this->roles = array_filter($this->roles, function ($value) use ($role) {
return $value != $role;
});
}
(Note that you will not be able to use type hinting unless you are using PHP 7 or above)