Search code examples
symfony4

Doctrine inverse side's collection field is randomly filled


I have the following two entities address and user . and in one of my controllers I have this function :

public function initAddressAction($idUser)
{
    $em = $this->getDoctrine()->getManager();
    $address = new Address();

    /** @var User $user*/
    $user= $em->getRepository('AppBundle:User' )->find($idUser);
    if ($user!== null) {
        $address->setUser($user); 
            dump($user); // #1
            $addresses = $user->getAddresses()->toArray();
            dump($user);die; // #2
            ...} 

My question is why the first dump prints the user object with an empty array in the addresses field :

#collection: Doctrine\Common\Collections\ArrayCollection {#9487 ▼

  -elements: []`

WHEREAS the second dump prints the user object with a non empty array collection in the addresses field (there is actually one address in this array):

#collection: Doctrine\Common\Collections\ArrayCollection {#9487 ▼

  -elements: array:1 [▼

    0 => App\Entity\address{#81625 ▼`

User :

/**
 * @ORM\OneToMany(targetEntity="App\Entity\Address", mappedBy="user")
 */
private $addresses;

/**
 * Set addresses
 *
 * @param Collection $addresses
 */
public function setAddresses($addresses)
{
    $this->addresses= $addresses;
}

/**
 * Get addresses
 *
 * @return \Doctrine\Common\Collections\Collection
 */
public function getAddresses()
{
    return $this->addresses;
}

/**
 * Add address
 *
 * @param Address $address
 * @return User
 */
public function addAddress(Address$address)
{
    if (!$this->addresses->contains($address)) {
        $this->addresses[] = $address;
    }

    return $this;
}

/**
 * Remove address
 *
 * @param Address $address
 */
public function removeAddress(Address $address)
{
    $this->addresses->removeElement($address);
}

Address :

/**
 * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="adresses")
 * @ORM\JoinColumn(name="id_user", referencedColumnName="id_user", nullable=false)
 */
private $user

/**
 * @return User
 */
public function getUser()
{
    return $this->user;
}

/**
 * @param User $user
 */
public function setUser($user)
{
    $this->user= $user;
}

Solution

  • I've generated a User and an Address class via the Symfony maker bundle.

    User:

    <?php
    
    namespace App\Entity;
    
    use App\Repository\UserRepository;
    use Doctrine\Common\Collections\ArrayCollection;
    use Doctrine\Common\Collections\Collection;
    use Doctrine\ORM\Mapping as ORM;
    
    /**
     * @ORM\Entity(repositoryClass=UserRepository::class)
     * @ORM\Table(name="`user`")
     */
    class User
    {
        /**
         * @ORM\Id
         * @ORM\GeneratedValue
         * @ORM\Column(type="integer")
         */
        private $id;
    
        /**
         * @ORM\Column(type="string", length=255)
         */
        private $name;
    
        /**
         * @ORM\OneToMany(targetEntity=Address::class, mappedBy="userField")
         */
        private $addresses;
    
        public function __construct()
        {
            $this->addresses = new ArrayCollection();
        }
    
        public function getId(): ?int
        {
            return $this->id;
        }
    
        public function getName(): ?string
        {
            return $this->name;
        }
    
        public function setName(string $name): self
        {
            $this->name = $name;
    
            return $this;
        }
    
        /**
         * @return Collection<int, Address>
         */
        public function getAddresses(): Collection
        {
            return $this->addresses;
        }
    
        public function addAddress(Address $address): self
        {
            if (!$this->addresses->contains($address)) {
                $this->addresses[] = $address;
                $address->setUserField($this);
            }
    
            return $this;
        }
    
        public function removeAddress(Address $address): self
        {
            if ($this->addresses->removeElement($address)) {
                // set the owning side to null (unless already changed)
                if ($address->getUserField() === $this) {
                    $address->setUserField(null);
                }
            }
    
            return $this;
        }
    }
    

    Address:

    <?php
    
    namespace App\Entity;
    
    use App\Repository\AddressRepository;
    use Doctrine\ORM\Mapping as ORM;
    
    /**
     * @ORM\Entity(repositoryClass=AddressRepository::class)
     */
    class Address
    {
        /**
         * @ORM\Id
         * @ORM\GeneratedValue
         * @ORM\Column(type="integer")
         */
        private $id;
    
        /**
         * @ORM\ManyToOne(targetEntity=User::class, inversedBy="addresses")
         * @ORM\JoinColumn(nullable=false)
         */
        private $userField;
    
        /**
         * @ORM\Column(type="string", length=255)
         */
        private $name;
    
        public function getId(): ?int
        {
            return $this->id;
        }
    
        public function getUserField(): ?User
        {
            return $this->userField;
        }
    
        public function setUserField(?User $userField): self
        {
            $this->userField = $userField;
    
            return $this;
        }
    
        public function getName(): ?string
        {
            return $this->name;
        }
    
        public function setName(string $name): self
        {
            $this->name = $name;
    
            return $this;
        }
    }
    

    Example that fails:

    Try this (in a controller for example):

        // get address 1 (this is attached to user 1 in the database)
        $address1 = $addressRepo->findOneBy(['id' => 1]);
        dump($address1);
    

    enter image description here

    This is Address 1, its User object has not been initalized yet, but that's not important for this example.

        // get user 2 (user 2 only has address 2 in the database)
        $user2 = $userRepo->findOneBy(['id' => 2]);
        dump($user2);
    

    enter image description here

    This is User 2, notice that the Address array is not initialized yet (this is important!).

        // set user 2 onto address 1
        $address1->setUserField($user2);
        dump($address1);
    

    enter image description here

    Now Address 1 has User 2.

        dump($user2);
    

    enter image description here

    But User 2 does not have Address 1.

        // load user 2's addresses from the database
        $user2->getAddresses()->toArray();
        dump($user2);
    

    enter image description here

    User 2 now has it's Address 2 loaded from the database (still no Address 1 though).

    With $userField->addAddress($this)

    If you add the $userField->addAddress($this); line:

    public function setUserField(?User $userField): self
    {
        $this->userField = $userField;
        $userField->addAddress($this);
    
        return $this;
    }
    

    And execute the same steps:

        // get address 1 (this is attached to user 1 in the database)
        $address1 = $addressRepo->findOneBy(['id' => 1]);
        dump($address1);
    
        // get user 2  (user 2 only has address 2 in the database)
        $user2 = $userRepo->findOneBy(['id' => 2]);
        dump($user2);
    
        // set user 2 onto address 1
        $address1->setUserField($user2);
        dump($address1);
        dump($user2);
    

    enter image description here

    User 2 now does have Address 1. Adding the Address to the User also triggers the loading of the existing Adresses from the database, so it now also has Address 2.

        // load user 2's addresses from the database
        $user2->getAddresses()->toArray();
        dump($user2);
    

    enter image description here

    (The Addresses were already loaded, no changes.)

    With persist & flush

    And if you do not add the $userField->addAddress($this); line, but add persist & flush:

        // get address 1 (this is attached to user 1 in the database)
        $address1 = $addressRepo->findOneBy(['id' => 1]);
        dump($address1);
    
        // get user 2  (user 2 only has address 2 in the database)
        $user2 = $userRepo->findOneBy(['id' => 2]);
        dump($user2);
    
        // set user 2 onto address 1
        $address1->setUserField($user2);
        dump($address1);
        dump($user2);
    

    enter image description here

    User 2 does not have Address 1.

        // persist address 1
        $em->persist($address1);
        $em->flush($address1);
    
        // load user 2's addresses from the database
        $user2->getAddresses()->toArray();
        dump($user2);
    

    enter image description here

    Now User 2 has both Address 1 and Address 2 loaded from the database.