diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 4c343f2ec93..5e340aa7df9 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1167,7 +1167,26 @@ private function executeInserts(): void $entities = $this->computeInsertExecutionOrder(); foreach ($entities as $entity) { - $oid = spl_object_id($entity); + $oid = spl_object_id($entity); + + // Mitigation for GH-10869: + // Users may use postPersist and similar listeners to make entity updates and call + // EM::flush() -> UoW::commit() again, while a transaction is currently running. + // This "somehow" worked pre 2.16, although it was never officially endorsed and/or + // is disputed (and there is no guarantee that the UoW will be able to deal with this, + // does not lose updates etc.). + // https://github.com/doctrine/orm/pull/10900 is a discussion about deprecating this + // kind of reentrance and disallowing it in 3.0. + // + // However, to ease the pain somewhat for users in 2.16, this condition covers that + // a reentrant call into UoW::commit() may have processed pending insertions that we + // had in our computed insertion order. So, after the second (inner) commit() returned + // and the outer one continues, deal with the situation that entities are no longer in + // the set of pending insertions. + if (! isset($this->entityInsertions[$oid])) { + continue; + } + $class = $this->em->getClassMetadata(get_class($entity)); $persister = $this->getEntityPersister($class->name); $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist); 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; +}