From bb36d49b38e1b3f7f198f0672ce4afaf9a07855b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mor=C3=A1vek?= Date: Fri, 26 Apr 2024 14:24:55 +0200 Subject: [PATCH] Keep entities in identity map until the scheduled deletions are executed. If the entity gets reloaded from database before the deletions are executed UnitOfWork needs to be able to return the original instance in REMOVED state. --- src/UnitOfWork.php | 4 +- .../ORM/Functional/Ticket/GH6123Test.php | 80 +++++++++++++++++++ tests/Tests/ORM/UnitOfWorkTest.php | 8 +- 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 tests/Tests/ORM/Functional/Ticket/GH6123Test.php diff --git a/src/UnitOfWork.php b/src/UnitOfWork.php index 609bea433f8..f1affcf7ebc 100644 --- a/src/UnitOfWork.php +++ b/src/UnitOfWork.php @@ -1292,6 +1292,8 @@ private function executeDeletions(): void $eventsToDispatch = []; foreach ($entities as $entity) { + $this->removeFromIdentityMap($entity); + $oid = spl_object_id($entity); $class = $this->em->getClassMetadata(get_class($entity)); $persister = $this->getEntityPersister($class->name); @@ -1667,8 +1669,6 @@ public function scheduleForDelete($entity) return; } - $this->removeFromIdentityMap($entity); - unset($this->entityUpdates[$oid]); if (! isset($this->entityDeletions[$oid])) { diff --git a/tests/Tests/ORM/Functional/Ticket/GH6123Test.php b/tests/Tests/ORM/Functional/Ticket/GH6123Test.php new file mode 100644 index 00000000000..7a5f14980f0 --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH6123Test.php @@ -0,0 +1,80 @@ +createSchemaForModels( + GH6123Entity::class, + ); + } + + public function testLoadingRemovedEntityFromDatabaseDoesNotCreateNewManagedEntityInstance(): void + { + $entity = new GH6123Entity(); + $this->_em->persist($entity); + $this->_em->flush(); + + self::assertSame(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($entity)); + self::assertFalse($this->_em->getUnitOfWork()->isScheduledForDelete($entity)); + + $this->_em->remove($entity); + + $freshEntity = $this->loadEntityFromDatabase($entity->id); + self::assertSame($entity, $freshEntity); + + self::assertSame(UnitOfWork::STATE_REMOVED, $this->_em->getUnitOfWork()->getEntityState($freshEntity)); + self::assertTrue($this->_em->getUnitOfWork()->isScheduledForDelete($freshEntity)); + } + + public function testRemovedEntityCanBePersistedAgain(): void + { + $entity = new GH6123Entity(); + $this->_em->persist($entity); + $this->_em->flush(); + + $this->_em->remove($entity); + self::assertSame(UnitOfWork::STATE_REMOVED, $this->_em->getUnitOfWork()->getEntityState($entity)); + self::assertTrue($this->_em->getUnitOfWork()->isScheduledForDelete($entity)); + + $this->loadEntityFromDatabase($entity->id); + + $this->_em->persist($entity); + self::assertSame(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($entity)); + self::assertFalse($this->_em->getUnitOfWork()->isScheduledForDelete($entity)); + + $this->_em->flush(); + } + + private function loadEntityFromDatabase(int $id): GH6123Entity|null + { + return $this->_em->createQueryBuilder() + ->select('e') + ->from(GH6123Entity::class, 'e') + ->where('e.id = :id') + ->setParameter('id', $id) + ->getQuery() + ->getOneOrNullResult(); + } +} + +#[ORM\Entity] +class GH6123Entity +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[Column(type: Types::INTEGER, nullable: false)] + public int $id; +} diff --git a/tests/Tests/ORM/UnitOfWorkTest.php b/tests/Tests/ORM/UnitOfWorkTest.php index 0569bfd06dd..ee475e729d0 100644 --- a/tests/Tests/ORM/UnitOfWorkTest.php +++ b/tests/Tests/ORM/UnitOfWorkTest.php @@ -413,12 +413,18 @@ public function testRemovedAndRePersistedEntitiesAreInTheIdentityMapAndAreNotGar $entity->id = 123; $this->_unitOfWork->registerManaged($entity, ['id' => 123], []); + self::assertSame(UnitOfWork::STATE_MANAGED, $this->_unitOfWork->getEntityState($entity)); + self::assertFalse($this->_unitOfWork->isScheduledForDelete($entity)); self::assertTrue($this->_unitOfWork->isInIdentityMap($entity)); $this->_unitOfWork->remove($entity); - self::assertFalse($this->_unitOfWork->isInIdentityMap($entity)); + self::assertSame(UnitOfWork::STATE_REMOVED, $this->_unitOfWork->getEntityState($entity)); + self::assertTrue($this->_unitOfWork->isScheduledForDelete($entity)); + self::assertTrue($this->_unitOfWork->isInIdentityMap($entity)); $this->_unitOfWork->persist($entity); + self::assertSame(UnitOfWork::STATE_MANAGED, $this->_unitOfWork->getEntityState($entity)); + self::assertFalse($this->_unitOfWork->isScheduledForDelete($entity)); self::assertTrue($this->_unitOfWork->isInIdentityMap($entity)); }