1
0
mirror of synced 2025-01-18 22:41:43 +03:00

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 :)
This commit is contained in:
Strate 2014-04-05 10:45:00 +04:00 committed by Marco Pivetta
parent f3b31c2807
commit 35ea399d33
4 changed files with 160 additions and 21 deletions

View File

@ -0,0 +1,108 @@
<?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\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 <strate@yandex.ru>
* @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);
}
}
}
}

View File

@ -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();
}
}

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;
@ -217,6 +218,17 @@ class PostLoadEventTest extends \Doctrine\Tests\OrmFunctionalTestCase
$this->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;
}
}