Updating (from the inverse side) bidirectional many-to-many relationships in Doctrine 2?

doctrine, doctrine-orm, orm, symfony, symfony-2.1

Solution

Ok, found the way, even if I'm not fully satisfied. The key was this example form collection field type. Basically what's happening with my previous form definition was:

$customer->getKeywords() = $postData; // $postData is somewhere in form framework

And that is just an assignment of a collection (of selected keywords) to customer keywords. No method were invoked on `Keyword` instances (owning side). The key option is `by_reference` (for me it's just a bad name, but anyways...):

$form
    ->add($this->factory->createNamed('entity', 'keywords', null, array(
        // ...
        'by_reference' => false
    ))
);

This way the form framework is going to call the setter, that is `$customer->setKeywords(Collection $keywords)`. In that method, you can "tell" the owning side to store your association:

public function setKeywords(Collection $keywords)
{
    foreach($keywords as $keyword) {
        $keyword->addCustomer($this); // Owning side call!
    }

    $this->keywords = $keywords;

    return $this;
}

(Always check for duplicate instances on the owning side, using `contains` method).

At this point, only checked keywords will be added (`$keyword` argument). There is the need to manage removal of unchecked keywords (controller side):

$originalKeywords = $customer->getKeywords()->toArray(); // When GET or POST

// When POST and form valid
$checkedKeywords = $customer->getKeywords()->toArray(); // Thanks to setKeywords

// Loop over all keywords
foreach($originalKeywords as $keyword) {
    if(!in_array($keyword, $checkedKeywords)) { // Keyword has been unchecked
        $keyword->removeCustomer($customer);
        $manager->persist($keyword);
    }
}

Ugly, but works. I would have the code for removal moved to the `Customer` class, but it's not possible at all. If you'll find a better solution, let me know!

Problem

`Customer` is the inverse side of "keywords/customers" relationship with `Keyword`: ``` /** * @ORM\ManyToMany(targetEntity="Keyword", mappedBy="customers", * cascade={"persist", "remove"} * ) */ protected $keywords; ``` When creating a new customer, one should select one or more keywords. The entity form field is: ``` $form->add($this->factory->createNamed('entity', 'keywords', null, array( 'class' => 'Acme\HelloBundle\Entity\Keyword', 'property' => 'select_label', 'multiple' => true, 'expanded' => true, ))); ``` In my controller code, after binding the request and check if form is valid, I need to persist both the customer and all customer/keyword(s) associations, that is the join table. However customer is the inverse side, so this is not working: ``` if($request->isPost()) { $form->bindRequest($request); if(!$form->isValid()) { return array('form' => $form->createView()); } // Valid form here $em = $this->getEntityManager(); $em->persist($customer); $em->flush(); } ``` Event with "cascade" option, this code fails. `$customer->getKeywords()` will return `Doctrine\ORM\PersistentCollection`, which holds only selected keywords. Should I manually check which keyword was removed/added and then update from the owning side?

Original source