How to update a manyToMany collection of an entity in onFlush event listener?

doctrine-orm, php, symfony

Solution

When updating an entity during a listener's onFlush event, all you need to call is `computeChangeSet()`:

// make changes to entity
$entity->field = 'value';

// or assign an existing entity to an assocation
$entity->user = $myExistingUserEntity;
$entity->tags->add($myExistingTagEntity);

$meta = $em->getClassMetadata(get_class($entity));
$uow->computeChangeSet($meta, $entity);

If you're creating other entities too, you need to persist them and compute their changes first!

$myNewUserEntity = new Entity\User;
$myNewTagEntity = new Entity\Tag;

$entity->user = $myNewUserEntity;
// make sure you call add() on the owning side for *ToMany associations
$entity->tags->add($myNewTagEntity);

$em->persist($myNewUserEntity);
$em->persist($myNewTagEntity);

$metaUser = $em->getClassMetadata(get_class($myNewUserEntity));
$uow->computeChangeSet($metaUser, $myNewUserEntity);

$metaTag = $em->getClassMetadata(get_class($myNewTagEntity));
$uow->computeChangeSet($metaTag, $myNewTagEntity);

$meta = $em->getClassMetadata(get_class($entity));
$uow->computeChangeSet($meta, $entity);

Problem

I have this entity: ``` <?php namespace Comakai\MyBundle\Entity; use Doctrine\ORM\Mapping as ORM, Symfony\Component\Validator\Constraints as Assert; /** * @ORM\Entity */ class Stuff { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="IDENTITY") */ private $id; /** * @ORM\Column(type="text") * @Assert\NotBlank() */ private $content; /** * @ORM\ManyToMany(targetEntity="Apple", cascade={"persist"}) */ private $apples; /** * @ORM\ManyToMany(targetEntity="Pig") */ private $pigs; public function __construct() { $this->apples = new \Doctrine\Common\Collections\ArrayCollection(); $this->pigs = new \Doctrine\Common\Collections\ArrayCollection(); } public function setApples($apples) { $this->getApples()->clear(); foreach ($apples as $apple) { $this->addApple($apple); } } public function setPigs($pigs) { $this->getPigs()->clear(); foreach ($pigs as $pig) { $this->addPig($pig); } } /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set content * * @param text $content */ public function setContent($content) { $this->content = $content; } /** * Get content * * @return text */ public function getContent() { return $this->content; } /** * Add apples * * @param Comakai\MyBundle\Entity\Apple $apples */ public function addApple(\Comakai\MyBundle\Entity\Apple $apples) { $this->apples[] = $apples; } /** * Get apples * * @return Doctrine\Common\Collections\Collection */ public function getApples() { return $this->apples; } /** * Add pigs * * @param Comakai\MyBundle\Entity\Pig $pigs */ public function addPig(\Comakai\MyBundle\Entity\Pig $pigs) { $this->pigs[] = $pigs; } /** * Get pigs * * @return Doctrine\Common\Collections\Collection */ public function getPigs() { return $this->pigs; } } ``` and this listener: ``` <?php namespace Comakai\MyBundle\Listener; use Comakai\MyBundle\Util\SluggerParser Doctrine\ORM\Event\OnFlushEventArgs, Comakai\MyBundle\Entity\Stuff, Comakai\MyBundle\Entity\Apple, Comakai\MyBundle\Entity\Pig; class Listener { /** * @param \Doctrine\ORM\Event\OnFlushEventArgs $ea */ public function onFlush(OnFlushEventArgs $ea) { $em = $ea->getEntityManager(); $uow = $em->getUnitOfWork(); foreach ($uow->getScheduledEntityInsertions() AS $entity) { $this->save($entity, $em, $uow); } foreach ($uow->getScheduledEntityUpdates() AS $entity) { $this->save($entity, $em, $uow); } } public function save($entity, $em, $uow) { if ($entity instanceof Stuff) { $pigRepository = $em->getRepository('Comakai\MyBundle\Entity\Pig'); $content = $entity->getContent(); preg_match_all('/@@ pig:(\d+) @@/i', $content, $matches); $entity->getPigs()->clear(); foreach($matches[1] as $pigID) { $pig = $pigRepository->find($pigID); if(!empty($pig)) { $entity->addPig($pig); } } $entity->setContent($content); $meta = $em->getClassMetadata(get_class($entity)); $uow->recomputeSingleEntityChangeSet($meta, $entity); $uow->computeChangeSet($meta, $entity); } } } ``` And it works fine if apple's collection is empty, but if it has some item I get a duplication error. How can I tell to the UnitOfWork that I only want to recalculate the pig's collection? UPDATE There is a new preFlush event (https://github.com/doctrine/doctrine2/pull/169) and I think this kind of things can be done there. That PR is not in the branch I'm using but let's try it!

Original source

Related problems