1
0
mirror of synced 2025-01-09 18:47:10 +03:00

Merge branch 'hotfix/#1001-DDC-3005-defer-postload-event-after-fully-populated-associations'

Close #1001
This commit is contained in:
Marco Pivetta 2015-01-13 00:54:31 +01:00
commit a906295c65
11 changed files with 436 additions and 21 deletions

View File

@ -184,13 +184,6 @@ the life-time of their registered entities.
invoked, after all references to entities have been removed from the unit of
work. This event is not a lifecycle callback.
.. warning::
Note that the postLoad event occurs for an entity
before any associations have been initialized. Therefore it is not
safe to access associations in a postLoad callback or event
handler.
.. warning::
Note that the postRemove event or any events triggered after an entity removal

View File

@ -96,6 +96,8 @@ class DefaultCollectionHydrator implements CollectionHydrator
$collection->hydrateSet($index, $entity);
});
$this->uow->hydrationComplete();
return $list;
}
}

View File

@ -151,6 +151,10 @@ 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;
}
}

View File

@ -149,6 +149,8 @@ class DefaultQueryCache implements QueryCache
$this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
}
$this->uow->hydrationComplete();
return null;
}
@ -176,6 +178,8 @@ class DefaultQueryCache implements QueryCache
$this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey);
}
$this->uow->hydrationComplete();
return null;
}
@ -196,6 +200,8 @@ class DefaultQueryCache implements QueryCache
$result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints);
}
$this->uow->hydrationComplete();
return $result;
}

View File

@ -145,8 +145,10 @@ class ObjectHydrator extends AbstractHydrator
$this->resultPointers = array();
if ($eagerLoad) {
$this->_em->getUnitOfWork()->triggerEagerLoads();
$this->_uow->triggerEagerLoads();
}
$this->_uow->hydrationComplete();
}
/**

View File

@ -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}
*/
@ -79,7 +90,7 @@ class SimpleObjectHydrator extends AbstractHydrator
if ($metaMappingDiscrColumnName = array_search($discrColumnName, $this->_rsm->metaMappings)) {
$discrColumnName = $metaMappingDiscrColumnName;
}
if ( ! isset($sqlResult[$discrColumnName])) {
throw HydrationException::missingDiscriminatorColumn($entityName, $discrColumnName, key($this->_rsm->aliasMap));
}

View File

@ -0,0 +1,103 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Internal;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\ListenersInvoker;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\ClassMetadata;
/**
* Class, which can handle completion of hydration cycle and produce some of tasks.
* In current implementation triggers deferred postLoad event.
*
* @author Artur Eshenbrener <strate@yandex.ru>
* @since 2.5
*/
final class HydrationCompleteHandler
{
/**
* @var ListenersInvoker
*/
private $listenersInvoker;
/**
* @var EntityManagerInterface
*/
private $em;
/**
* @var array[]
*/
private $deferredPostLoadInvocations = array();
/**
* Constructor for this object
*
* @param ListenersInvoker $listenersInvoker
* @param EntityManagerInterface $em
*/
public function __construct(ListenersInvoker $listenersInvoker, EntityManagerInterface $em)
{
$this->listenersInvoker = $listenersInvoker;
$this->em = $em;
}
/**
* Method schedules invoking of postLoad entity to the very end of current hydration cycle.
*
* @param ClassMetadata $class
* @param object $entity
*/
public function deferPostLoadInvoking(ClassMetadata $class, $entity)
{
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postLoad);
if ($invoke === ListenersInvoker::INVOKE_NONE) {
return;
}
$this->deferredPostLoadInvocations[] = array($class, $invoke, $entity);
}
/**
* This method should me called after any hydration cycle completed.
*
* Method fires all deferred invocations of postLoad events
*/
public function hydrationComplete()
{
$toInvoke = $this->deferredPostLoadInvocations;
$this->deferredPostLoadInvocations = array();
foreach ($toInvoke as $classAndEntity) {
list($class, $invoke, $entity) = $classAndEntity;
$this->listenersInvoker->invoke(
$class,
Events::postLoad,
$entity,
new LifecycleEventArgs($entity, $this->em),
$invoke
);
}
}
}

View File

@ -20,6 +20,7 @@
namespace Doctrine\ORM;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Internal\HydrationCompleteHandler;
use Exception;
use InvalidArgumentException;
use UnexpectedValueException;
@ -276,6 +277,13 @@ class UnitOfWork implements PropertyChangedListener
*/
protected $hasCache = false;
/**
* Helper for handling completion of hydration
*
* @var HydrationCompleteHandler
*/
private $hydrationCompleteHandler;
/**
* Initializes a new UnitOfWork instance, bound to the given EntityManager.
*
@ -283,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->listenersInvoker, $em);
}
/**
@ -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->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
}
return $entity;
@ -3379,4 +3385,15 @@ 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()
{
$this->hydrationCompleteHandler->hydrationComplete();
}
}

View File

@ -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(

View File

@ -1,6 +1,7 @@
<?php
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
use Doctrine\Tests\Models\CMS\CmsAddress;
@ -203,6 +204,37 @@ class PostLoadEventTest extends \Doctrine\Tests\OrmFunctionalTestCase
$phonenumbersCol->first();
}
/**
* @group DDC-3005
*/
public function testAssociationsArePopulatedWhenEventIsFired()
{
$checkerListener = new PostLoadListenerCheckAssociationsArePopulated();
$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');
}
/**
* @group DDC-3005
*/
public function testEventRaisedCorrectTimesWhenOtherEntityLoadedInEventHandler()
{
$eventManager = $this->_em->getEventManager();
$listener = new PostLoadListenerLoadEntityInEventHandler();
$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;
@ -248,3 +280,46 @@ class PostLoadListener
echo 'Should never be called!';
}
}
class PostLoadListenerCheckAssociationsArePopulated
{
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();
}
}
}
class PostLoadListenerLoadEntityInEventHandler
{
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;
}
}

View File

@ -0,0 +1,199 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\Tests\ORM\Internal;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\ListenersInvoker;
use Doctrine\ORM\Events;
use Doctrine\ORM\Internal\HydrationCompleteHandler;
use PHPUnit_Framework_TestCase;
use stdClass;
/**
* Tests for {@see \Doctrine\ORM\Internal\HydrationCompleteHandler}
*
* @covers \Doctrine\ORM\Internal\HydrationCompleteHandler
*/
class HydrationCompleteHandlerTest extends PHPUnit_Framework_TestCase
{
/**
* @var \Doctrine\ORM\Event\ListenersInvoker|\PHPUnit_Framework_MockObject_MockObject
*/
private $listenersInvoker;
/**
* @var \Doctrine\ORM\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
private $entityManager;
/**
* @var HydrationCompleteHandler
*/
private $handler;
/**
* {@inheritDoc}
*/
protected function setUp()
{
$this->listenersInvoker = $this->getMock('Doctrine\ORM\Event\ListenersInvoker', array(), array(), '', false);
$this->entityManager = $this->getMock('Doctrine\ORM\EntityManagerInterface');
$this->handler = new HydrationCompleteHandler($this->listenersInvoker, $this->entityManager);
}
/**
* @dataProvider testGetValidListenerInvocationFlags
*
* @param int $listenersFlag
*/
public function testDefersPostLoadOfEntity($listenersFlag)
{
/* @var $metadata \Doctrine\ORM\Mapping\ClassMetadata */
$metadata = $this->getMock('Doctrine\ORM\Mapping\ClassMetadata', array(), array(), '', false);
$entity = new stdClass();
$entityManager = $this->entityManager;
$this
->listenersInvoker
->expects($this->any())
->method('getSubscribedSystems')
->with($metadata)
->will($this->returnValue($listenersFlag));
$this->handler->deferPostLoadInvoking($metadata, $entity);
$this
->listenersInvoker
->expects($this->once())
->method('invoke')
->with(
$metadata,
Events::postLoad,
$entity,
$this->callback(function (LifecycleEventArgs $args) use ($entityManager, $entity) {
return $entity === $args->getEntity() && $entityManager === $args->getObjectManager();
}),
$listenersFlag
);
$this->handler->hydrationComplete();
}
/**
* @dataProvider testGetValidListenerInvocationFlags
*
* @param int $listenersFlag
*/
public function testDefersPostLoadOfEntityOnlyOnce($listenersFlag)
{
/* @var $metadata \Doctrine\ORM\Mapping\ClassMetadata */
$metadata = $this->getMock('Doctrine\ORM\Mapping\ClassMetadata', array(), array(), '', false);
$entity = new stdClass();
$this
->listenersInvoker
->expects($this->any())
->method('getSubscribedSystems')
->with($metadata)
->will($this->returnValue($listenersFlag));
$this->handler->deferPostLoadInvoking($metadata, $entity);
$this->listenersInvoker->expects($this->once())->method('invoke');
$this->handler->hydrationComplete();
$this->handler->hydrationComplete();
}
/**
* @dataProvider testGetValidListenerInvocationFlags
*
* @param int $listenersFlag
*/
public function testDefersMultiplePostLoadOfEntity($listenersFlag)
{
/* @var $metadata1 \Doctrine\ORM\Mapping\ClassMetadata */
/* @var $metadata2 \Doctrine\ORM\Mapping\ClassMetadata */
$metadata1 = $this->getMock('Doctrine\ORM\Mapping\ClassMetadata', array(), array(), '', false);
$metadata2 = $this->getMock('Doctrine\ORM\Mapping\ClassMetadata', array(), array(), '', false);
$entity1 = new stdClass();
$entity2 = new stdClass();
$entityManager = $this->entityManager;
$this
->listenersInvoker
->expects($this->any())
->method('getSubscribedSystems')
->with($this->logicalOr($metadata1, $metadata2))
->will($this->returnValue($listenersFlag));
$this->handler->deferPostLoadInvoking($metadata1, $entity1);
$this->handler->deferPostLoadInvoking($metadata2, $entity2);
$this
->listenersInvoker
->expects($this->exactly(2))
->method('invoke')
->with(
$this->logicalOr($metadata1, $metadata2),
Events::postLoad,
$this->logicalOr($entity1, $entity2),
$this->callback(function (LifecycleEventArgs $args) use ($entityManager, $entity1, $entity2) {
return in_array($args->getEntity(), array($entity1, $entity2), true)
&& $entityManager === $args->getObjectManager();
}),
$listenersFlag
);
$this->handler->hydrationComplete();
}
public function testSkipsDeferredPostLoadOfMetadataWithNoInvokedListeners()
{
/* @var $metadata \Doctrine\ORM\Mapping\ClassMetadata */
$metadata = $this->getMock('Doctrine\ORM\Mapping\ClassMetadata', array(), array(), '', false);
$entity = new stdClass();
$this
->listenersInvoker
->expects($this->any())
->method('getSubscribedSystems')
->with($metadata)
->will($this->returnValue(ListenersInvoker::INVOKE_NONE));
$this->handler->deferPostLoadInvoking($metadata, $entity);
$this->listenersInvoker->expects($this->never())->method('invoke');
$this->handler->hydrationComplete();
}
public function testGetValidListenerInvocationFlags()
{
return array(
array(ListenersInvoker::INVOKE_LISTENERS),
array(ListenersInvoker::INVOKE_CALLBACKS),
array(ListenersInvoker::INVOKE_MANAGER),
array(ListenersInvoker::INVOKE_LISTENERS | ListenersInvoker::INVOKE_CALLBACKS),
array(ListenersInvoker::INVOKE_LISTENERS | ListenersInvoker::INVOKE_MANAGER),
array(ListenersInvoker::INVOKE_LISTENERS | ListenersInvoker::INVOKE_CALLBACKS | ListenersInvoker::INVOKE_MANAGER),
);
}
}