Search code examples
symfonyarraycollection

Symfony 4 : ArrayCollection add not persisting in database


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


Solution

  • 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)