diff --git a/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php b/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php index a74b88dfb..cedd891da 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php @@ -96,6 +96,8 @@ class DefaultCollectionHydrator implements CollectionHydrator $collection->hydrateSet($index, $entity); }); + $this->uow->hydrationComplete(); + return $list; } } diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php index 6f22900e1..b040dfa92 100644 --- a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -151,6 +151,9 @@ class DefaultEntityHydrator implements EntityHydrator $this->uow->registerManaged($entity, $key->identifier, $data); } - return $this->uow->createEntity($entry->class, $data, $hints); + $result = $this->uow->createEntity($entry->class, $data, $hints); + $this->uow->hydrationComplete(); + + return $result; } } diff --git a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php index 927d46ebe..6d0c35492 100644 --- a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php @@ -148,6 +148,7 @@ class DefaultQueryCache implements QueryCache if ($this->cacheLogger !== null) { $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey); } + $this->uow->hydrationComplete(); return null; } @@ -175,6 +176,7 @@ class DefaultQueryCache implements QueryCache if ($this->cacheLogger !== null) { $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey); } + $this->uow->hydrationComplete(); return null; } @@ -196,6 +198,8 @@ class DefaultQueryCache implements QueryCache $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints); } + $this->uow->hydrationComplete(); + return $result; } diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index d4e84da92..ecbf9dcc4 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -145,8 +145,10 @@ class ObjectHydrator extends AbstractHydrator $this->resultPointers = array(); if ($eagerLoad) { - $this->_em->getUnitOfWork()->triggerEagerLoads(); + $this->_uow->triggerEagerLoads(); } + + $this->_uow->hydrationComplete(); } /** diff --git a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php index ea2664291..69fd60ca4 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php @@ -47,6 +47,17 @@ class SimpleObjectHydrator extends AbstractHydrator $this->class = $this->getClassMetadata(reset($this->_rsm->aliasMap)); } + /** + * {@inheritdoc} + */ + protected function cleanup() + { + parent::cleanup(); + + $this->_uow->triggerEagerLoads(); + $this->_uow->hydrationComplete(); + } + /** * {@inheritdoc} */ diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index c0e681bce..fcb3cf652 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -276,6 +276,15 @@ 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 + * + * @var array + */ + private $deferredToInvokeLoadEventEntities = array(); + /** * Initializes a new UnitOfWork instance, bound to the given EntityManager. * @@ -2801,11 +2810,8 @@ class UnitOfWork implements PropertyChangedListener } if ($overrideLocalValues) { - $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); - } + // defer invoking of postLoad event to hydration complete step + $this->deferredToInvokeLoadEventEntities[] = array($class, $entity); } return $entity; @@ -3379,4 +3385,23 @@ class UnitOfWork implements PropertyChangedListener return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2); } + + /** + * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle. + * Unit of work able to fire deferred events, related to loading events here. + * + * @internal should be called internally from object hydrators + */ + 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(); + } } diff --git a/tests/Doctrine/Tests/ORM/Functional/PostLoadEventTest.php b/tests/Doctrine/Tests/ORM/Functional/PostLoadEventTest.php index 6cc464bab..d041faafc 100644 --- a/tests/Doctrine/Tests/ORM/Functional/PostLoadEventTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/PostLoadEventTest.php @@ -203,6 +203,20 @@ class PostLoadEventTest extends \Doctrine\Tests\OrmFunctionalTestCase $phonenumbersCol->first(); } + public function testAssociationsArePopulatedWhenEventIsFired() + { + $checkerListener = new PostLoadListener_CheckAssociationsArePopulated(); + $this->_em->getEventManager()->addEventListener(array(Events::postLoad), $checkerListener); + + $qb = $this->_em->getRepository('Doctrine\Tests\Models\CMS\CmsUser')->createQueryBuilder('u'); + $qb->leftJoin('u.email', 'email'); + $qb->addSelect('email'); + $qb->getQuery()->getSingleResult(); + + $this->assertTrue($checkerListener->checked, 'postLoad event is not invoked'); + $this->assertTrue($checkerListener->populated, 'Association of email is not populated in postLoad event'); + } + private function loadFixture() { $user = new CmsUser; @@ -248,3 +262,22 @@ class PostLoadListener echo 'Should never be called!'; } } + +class PostLoadListener_CheckAssociationsArePopulated +{ + public $checked = false; + + public $populated = false; + + public function postLoad(LifecycleEventArgs $event) + { + $object = $event->getObject(); + if ($object instanceof CmsUser) { + if ($this->checked) { + throw new \RuntimeException('Expected to be one user!'); + } + $this->checked = true; + $this->populated = null !== $object->getEmail(); + } + } +}