Symfony 4 : ArrayCollection add not persisting in database Symfony 4 : ArrayCollection add not persisting in database symfony symfony

Symfony 4 : ArrayCollection add not persisting in database


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)


I believe this problems comes from doctrine internal workings.

Start with this: Value object should be immutable. Remember this well, because it will help you understand how doctrine works.

So what happens is, you create a new value object(ArrayCollection) with ROLE_USER, which gets serialized and saved in the database.

When you fetch your entity, you will get back your value object. However, simply adding more items to the collection won't change it's hash, and that is what doctrine cares about.

Therefore your changes are not recognized.

This behavior is universal in doctrine, as far as value objects are concerned. Read it here: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/cookbook/working-with-datetime.html

Relations work well with collections, because they are prepared to handle this, as they don't work with object hashes alone.(In fact since doctrine 2.6 you should not swap out collections, hashes should stay the same. https://github.com/doctrine/doctrine2/pull/7219)

Fix: use a simple array to save roles, with the simple_array type.

OR: Creating the adder like this will trigger the save:

public function addRole(string $role){    $this->roles->add($role);    $this->roles = clone $this->roles; // this will trigger the db update, as it creates a new object hash.}


Change roles attribute in User entity accordingly and don't forget to update your schema:

public function __construct(){    $this->roles = new ArrayCollection();    $this->addRole("ROLE_USER");}/** * @var ArrayCollection * @ORM\Column(name="roles", type="array", nullable=true) */private $roles;/** * Returns the roles granted to the user. * * @return (Role|string)[] The user roles */public function getRoles(){    return $this->roles->toArray();}public function addRole($role){    $this->roles->add($role);    return $this;}public function removeRole($role){    $this->roles->removeElement($role);    $this->roles = clone $this->roles;}