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;}