Search code examples
symfonydoctrinemany-to-manyrelational-database

Doctrine ManyToMany self referencing bidirectionnal - Parent not updated


I'm trying to create a ManyToMany relation beetwin services of a company. Each service had N parents services and N children services.

I looked at the doctrine documentation here : Many-To-Many, Self-referencing and I implemented it as followed :

Here is my service entity :

<?
namespace AppBundle\Entity;

class Service
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", mappedBy="enfants", cascade={"persist"})
     */
    private $parents;

    /**
     * @ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", inversedBy="parents")
     * @ORM\JoinTable(name="app_services_hierarchy",
     *      joinColumns={@ORM\JoinColumn(name="parent_id", referencedColumnName="id")},
     *      inverseJoinColumns={@ORM\JoinColumn(name="enfant_id", referencedColumnName="id")}
     *      )
     */
    private $enfants;

    public function __construct()
    {
        $this->enfants  = new ArrayCollection();
        $this->parents  = new ArrayCollection();
    }

    public function getId(){
        return $this->id;
    }

    //--------------------------------------------------Enfants
    public function getEnfants(){
        return $this->enfants;
    }

    public function setEnfants($enfant){
        $this->enfants = $enfant;
    }

    public function addEnfant(Service $s){
        $this->enfants[] = $s;
        return $this;
    }

    public function removeEnfant(Service $s){
        $this->enfants->removeElement($s);
    }

    //--------------------------------------------------Parents
    public function getParents(){
        return $this->parents;
    }

    public function setParents($parents){
        $this->parents = $parents;
    }

    public function addParent(Service $s){
        $this->parents[] = $s;
        return $this;
    }

    public function removeParent(Service $s){
        $this->parents->removeElement($s);
    }

}

And here is my edit function( Controller.php) :

public function editAction(Request $request, $id)
{
    $service  = $this->getDoctrine()->getRepository(Service::class)->find($id);
    $form     = $this->createForm(ServiceType::class, $service);
    $form     ->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {

        $entityManager = $this->getDoctrine()->getManager();
        $entityManager ->persist($service);

        dump($service);

        $entityManager ->flush();
    }

    return $this->render('AppBundle:Service:edit.html.twig', array(
        'form'      => $form->createView(),
    ));
}

And the generated form looks like :

ServiceType generated

PROBLEM :

My problem is that the childrens are updated but not the parents. I can see the parents in the $service variable when I dump() it in my controler but the only ones updated in my database table (app_services_hierarchie) are the children.


Solution

  • The difference between $parents and $enfants in your code is that the service you are looking at is the Owning side in case of your $enfants mapping, but not in the case of your $parents mapping.

    Doctrine will not store the $parents unless you tell it to do so via cascade={"persist"}.

    /**
     * @ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", mappedBy="enfants", cascade={"persist"})
     */
    

    This is basically the same anwer given in the post linked by @GregoireDucharme.

    Edit: after some research, apparently this problem cannot be solved using cascade. According to the Doctrine documentation:

    Doctrine will only check the owning side of an association for changes.

    So what you have to do is tell your $parents to also update the $children property.

    public function addParent(Service $s){
        $this->parents[] = $s;
        $s->addEnfant($this);
        return $this;
    }
    
    public function removeParent(Service $s){
        $this->parents->removeElement($s);
        $s->removeEnfant($this);
    }
    

    In your form, make sure to specify the following:

    ->add('parents', 'collection', array(
        'by_reference' => false,
        //...
    ))
    

    (I haven't spellchecked any of the code above, so tread carefully.)

    If 'by_reference' is set to true, addParent and removeParent will not be called.

    Credit goes to this blog post by Anny Filina.

    It also states that you can remove the cascade option from your $parents property, but you probably should add cascade={"persist","remove"} to your $enfants property.