diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 4c343f2ec93..bac9c7ac51b 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1165,12 +1165,12 @@ public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity) private function executeInserts(): void { $entities = $this->computeInsertExecutionOrder(); + $eventsToDispatch = []; foreach ($entities as $entity) { $oid = spl_object_id($entity); $class = $this->em->getClassMetadata(get_class($entity)); $persister = $this->getEntityPersister($class->name); - $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist); $persister->addInsert($entity); @@ -1197,10 +1197,19 @@ private function executeInserts(): void $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity); } + $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist); + if ($invoke !== ListenersInvoker::INVOKE_NONE) { - $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new PostPersistEventArgs($entity, $this->em), $invoke); + $eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke]; } } + + // Mitigation for GH-10869: + // Defer dispatching `postPersist` events to until all entities have been inserted and there + // are no pending insertions left. + foreach ($eventsToDispatch as $event) { + $this->listenersInvoker->invoke($event['class'], Events::postPersist, $event['entity'], new PostPersistEventArgs($event['entity'], $this->em), $event['invoke']); + } } /** diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10869Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10869Test.php new file mode 100644 index 00000000000..cd99496682c --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10869Test.php @@ -0,0 +1,75 @@ +setUpEntitySchema([ + GH10869Entity::class, + ]); + } + + public function testPostPersistListenerUpdatingObjectFieldWhileOtherInsertPending(): void + { + $entity1 = new GH10869Entity(); + $this->_em->persist($entity1); + + $entity2 = new GH10869Entity(); + $this->_em->persist($entity2); + + $this->_em->getEventManager()->addEventListener(Events::postPersist, new class { + public function postPersist(PostPersistEventArgs $args): void + { + $object = $args->getObject(); + + $objectManager = $args->getObjectManager(); + $object->field = sprintf('test %s', $object->id); + $objectManager->flush(); + } + }); + + $this->_em->flush(); + $this->_em->clear(); + + $entity1Reloaded = $this->_em->find(GH10869Entity::class, $entity1->id); + self::assertSame($entity1->field, $entity1Reloaded->field); + + $entity2Reloaded = $this->_em->find(GH10869Entity::class, $entity2->id); + self::assertSame($entity2->field, $entity2Reloaded->field); + } +} + +/** + * @ORM\Entity + */ +class GH10869Entity +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + * + * @var ?int + */ + public $id; + + /** + * @ORM\Column(type="text", nullable=true) + * + * @var ?string + */ + public $field; +}