From 35ea399d3346ee0da483b72fa54b773953187150 Mon Sep 17 00:00:00 2001 From: Strate Date: Sat, 5 Apr 2014 10:45:00 +0400 Subject: [PATCH] DDC-3005 Defer invoking of postLoad event to the end of hydration cycle. 1. Refactor handling of hydration complete: delegate this task to special object 2. Write test case for situation, when inside postLoad listener other entity is loading. 3. Make test, written on second step, be able to pass :) --- .../ORM/Internal/HydrationCompleteHandler.php | 108 ++++++++++++++++++ lib/Doctrine/ORM/UnitOfWork.php | 32 ++---- tests/Doctrine/Tests/Models/CMS/CmsUser.php | 5 +- .../ORM/Functional/PostLoadEventTest.php | 36 ++++++ 4 files changed, 160 insertions(+), 21 deletions(-) create mode 100644 lib/Doctrine/ORM/Internal/HydrationCompleteHandler.php diff --git a/lib/Doctrine/ORM/Internal/HydrationCompleteHandler.php b/lib/Doctrine/ORM/Internal/HydrationCompleteHandler.php new file mode 100644 index 000000000..09ea8f9cd --- /dev/null +++ b/lib/Doctrine/ORM/Internal/HydrationCompleteHandler.php @@ -0,0 +1,108 @@ +. + */ + +namespace Doctrine\ORM\Internal; + +use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Event\LifecycleEventArgs; +use Doctrine\ORM\Event\ListenersInvoker; +use Doctrine\ORM\Events; +use Doctrine\ORM\UnitOfWork; + +/** + * Class, which can handle completion of hydration cycle and produce some of tasks. + * In current implementation triggers deferred postLoad event. + * + * TODO Move deferred eager loading here + * + * @author Artur Eshenbrener + * @since 2.5 + */ +class HydrationCompleteHandler +{ + /** @var \Doctrine\ORM\UnitOfWork */ + private $uow; + + /** @var \Doctrine\ORM\Event\ListenersInvoker */ + private $listenersInvoker; + + /** @var \Doctrine\ORM\EntityManager */ + private $em; + + /** @var array */ + private $deferredPostLoadInvocations = array(); + + /** + * Constructor for this object + * + * @param UnitOfWork $uow + * @param \Doctrine\ORM\Event\ListenersInvoker $listenersInvoker + * @param \Doctrine\ORM\EntityManager $em + * + * @since 2.5 + */ + public function __construct(UnitOfWork $uow, ListenersInvoker $listenersInvoker, EntityManager $em) + { + $this->uow = $uow; + $this->listenersInvoker = $listenersInvoker; + $this->em = $em; + } + + /** + * Method schedules invoking of postLoad entity to the very end of current hydration cycle. + * + * @since 2.5 + * + * @param ClassMetadata $class + * @param object $entity + */ + public function deferPostLoadInvoking(ClassMetadata $class, $entity) + { + $this->deferredPostLoadInvocations[] = array($class, $entity); + } + + /** + * This method should me called after any hydration cycle completed. + * @since 2.5 + */ + public function hydrationComplete() + { + $this->invokeAllDeferredPostLoadEvents(); + } + + /** + * Method fires all deferred invocations of postLoad events + * @since 2.5 + */ + private function invokeAllDeferredPostLoadEvents() + { + $toInvoke = $this->deferredPostLoadInvocations; + $this->deferredPostLoadInvocations = array(); + foreach ($toInvoke as $classAndEntity) { + list($class, $entity) = $classAndEntity; + + $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postLoad); + + if ($invoke !== ListenersInvoker::INVOKE_NONE) { + $this->listenersInvoker->invoke($class, Events::postLoad, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); + } + } + } +} diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index fcb3cf652..0d126c84e 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -20,6 +20,7 @@ namespace Doctrine\ORM; use Doctrine\DBAL\LockMode; +use Doctrine\ORM\Internal\HydrationCompleteHandler; use Exception; use InvalidArgumentException; use UnexpectedValueException; @@ -277,13 +278,11 @@ class UnitOfWork implements PropertyChangedListener protected $hasCache = false; /** - * Map of entities, loaded in current hydration cycle. - * After hydration cycle is finished, some of events should be fired for this entities. - * Array contains arrays of two values. First value is ClassMetadata object, second is entity object + * Helper for handling completion of hydration * - * @var array + * @var HydrationCompleteHandler */ - private $deferredToInvokeLoadEventEntities = array(); + private $hydrationCompleteHandler; /** * Initializes a new UnitOfWork instance, bound to the given EntityManager. @@ -292,11 +291,12 @@ class UnitOfWork implements PropertyChangedListener */ public function __construct(EntityManager $em) { - $this->em = $em; - $this->evm = $em->getEventManager(); - $this->listenersInvoker = new ListenersInvoker($em); - $this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled(); - $this->identifierFlattener = new IdentifierFlattener($this, $em->getMetadataFactory()); + $this->em = $em; + $this->evm = $em->getEventManager(); + $this->listenersInvoker = new ListenersInvoker($em); + $this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled(); + $this->identifierFlattener = new IdentifierFlattener($this, $em->getMetadataFactory()); + $this->hydrationCompleteHandler = new HydrationCompleteHandler($this, $this->listenersInvoker, $em); } /** @@ -2811,7 +2811,7 @@ class UnitOfWork implements PropertyChangedListener if ($overrideLocalValues) { // defer invoking of postLoad event to hydration complete step - $this->deferredToInvokeLoadEventEntities[] = array($class, $entity); + $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity); } return $entity; @@ -3394,14 +3394,6 @@ class UnitOfWork implements PropertyChangedListener */ public function hydrationComplete() { - foreach ($this->deferredToInvokeLoadEventEntities as $metaAndEntity) { - list($class, $entity) = $metaAndEntity; - $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postLoad); - - if ($invoke !== ListenersInvoker::INVOKE_NONE) { - $this->listenersInvoker->invoke($class, Events::postLoad, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); - } - } - $this->deferredToInvokeLoadEventEntities = array(); + $this->hydrationCompleteHandler->hydrationComplete(); } } diff --git a/tests/Doctrine/Tests/Models/CMS/CmsUser.php b/tests/Doctrine/Tests/Models/CMS/CmsUser.php index 2bae6ed4f..c95cce4ea 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsUser.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsUser.php @@ -236,6 +236,9 @@ class CmsUser } } + /** + * @return CmsEmail + */ public function getEmail() { return $this->email; } public function setEmail(CmsEmail $email = null) { @@ -387,7 +390,7 @@ class CmsUser ) ) )); - + $metadata->addSqlResultSetMapping(array( 'name' => 'mappingMultipleJoinsEntityResults', 'entities' => array(array( diff --git a/tests/Doctrine/Tests/ORM/Functional/PostLoadEventTest.php b/tests/Doctrine/Tests/ORM/Functional/PostLoadEventTest.php index d041faafc..302b3b129 100644 --- a/tests/Doctrine/Tests/ORM/Functional/PostLoadEventTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/PostLoadEventTest.php @@ -1,6 +1,7 @@ assertTrue($checkerListener->populated, 'Association of email is not populated in postLoad event'); } + public function testEventRaisedCorrectTimesWhenOtherEntityLoadedInEventHandler() + { + $eventManager = $this->_em->getEventManager(); + $listener = new PostLoadListener_LoadEntityInEventHandler(); + $eventManager->addEventListener(array(Events::postLoad), $listener); + + $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + $this->assertSame(1, $listener->countHandledEvents('Doctrine\Tests\Models\CMS\CmsUser'), 'Doctrine\Tests\Models\CMS\CmsUser should be handled once!'); + $this->assertSame(1, $listener->countHandledEvents('Doctrine\Tests\Models\CMS\CmsEmail'), '\Doctrine\Tests\Models\CMS\CmsEmail should be handled once!'); + } + private function loadFixture() { $user = new CmsUser; @@ -281,3 +293,27 @@ class PostLoadListener_CheckAssociationsArePopulated } } } + +class PostLoadListener_LoadEntityInEventHandler +{ + private $firedByClasses = array(); + + public function postLoad(LifecycleEventArgs $event) + { + $object = $event->getObject(); + $class = ClassUtils::getClass($object); + if (!isset($this->firedByClasses[$class])) { + $this->firedByClasses[$class] = 1; + } else { + $this->firedByClasses[$class]++; + } + if ($object instanceof CmsUser) { + $object->getEmail()->getEmail(); + } + } + + public function countHandledEvents($className) + { + return isset($this->firedByClasses[$className]) ? $this->firedByClasses[$className] : 0; + } +}