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; + } +}