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?