diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index b3285a8845c..d6f55ceb573 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -469,6 +469,8 @@ public function commit($entity = null) // (first delete entities depending upon others, before deleting depended-upon entities). if ($this->entityDeletions) { $this->executeDeletions(); + } else { + $this->finalizeCollectionUpdates(); } // Commit failed silently @@ -490,7 +492,12 @@ public function commit($entity = null) } $this->afterTransactionComplete(); + $this->dispatchPostFlushEvent(); + $this->postCommitCleanup($entity); + } + private function finalizeCollectionUpdates(): void + { // Unset removed entities from collections, and take new snapshots from // all visited collections. foreach ($this->visitedCollections as $coid => $coll) { @@ -502,10 +509,6 @@ public function commit($entity = null) $coll->takeSnapshot(); } - - $this->dispatchPostFlushEvent(); - - $this->postCommitCleanup($entity); } /** @param object|object[]|null $entity */ @@ -1317,6 +1320,8 @@ private function executeDeletions(): void } } + $this->finalizeCollectionUpdates(); + // Defer dispatching `postRemove` events to until all entities have been removed. foreach ($eventsToDispatch as $event) { $this->listenersInvoker->invoke( diff --git a/tests/Doctrine/Tests/ORM/Functional/EntityListenersTest.php b/tests/Doctrine/Tests/ORM/Functional/EntityListenersTest.php index 908cb31059d..2dc10efe29f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/EntityListenersTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/EntityListenersTest.php @@ -4,6 +4,7 @@ namespace Doctrine\Tests\ORM\Functional; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Event\PostPersistEventArgs; use Doctrine\ORM\Event\PostRemoveEventArgs; use Doctrine\ORM\Event\PreFlushEventArgs; @@ -12,6 +13,7 @@ use Doctrine\ORM\UnitOfWork; use Doctrine\Persistence\Event\LifecycleEventArgs; use Doctrine\Tests\Models\Company\CompanyContractListener; +use Doctrine\Tests\Models\Company\CompanyEmployee; use Doctrine\Tests\Models\Company\CompanyFixContract; use Doctrine\Tests\Models\Company\CompanyPerson; use Doctrine\Tests\OrmFunctionalTestCase; @@ -266,4 +268,44 @@ public function postRemove(PostRemoveEventArgs $args): void self::assertSame(2, $listener->invocationCount); } + + public function testPostRemoveCalledAfterAllInMemoryCollectionsHaveBeenUpdated(): void + { + $contract = new CompanyFixContract(); + $contract->setFixPrice(2000); + + $engineer = new CompanyEmployee(); + $engineer->setName('J. Doe'); + $engineer->setSalary(50); + $engineer->setDepartment('tech'); + + $contract->addEngineer($engineer); + $engineer->contracts = new ArrayCollection([$contract]); + + $this->_em->persist($contract); + $this->_em->persist($engineer); + $this->_em->flush(); + + $this->_em->getEventManager()->addEventListener([Events::postRemove], new class ($contract) { + /** @var CompanyFixContract */ + private $contract; + + public function __construct(CompanyFixContract $contract) + { + $this->contract = $contract; + } + + public function postRemove(): void + { + Assert::assertEmpty($this->contract->getEngineers()); // Assert collection has been updated before event was dispatched + Assert::assertFalse($this->contract->getEngineers()->isDirty()); // Collections are clean at this point + } + }); + + $this->_em->remove($engineer); + $this->_em->flush(); + + self::assertEmpty($contract->getEngineers()); + self::assertFalse($contract->getEngineers()->isDirty()); + } }