diff --git a/composer.json b/composer.json index 3b90dff5c82..06748249fb8 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "doctrine/collections": "~1.2", "doctrine/dbal": ">=2.5-dev,<2.6-dev", "doctrine/instantiator": "~1.0.1", + "doctrine/common": ">=2.5-dev,<2.6-dev", "symfony/console": "~2.5" }, "require-dev": { diff --git a/docs/en/reference/events.rst b/docs/en/reference/events.rst index 65acd246602..6ec2f092db3 100644 --- a/docs/en/reference/events.rst +++ b/docs/en/reference/events.rst @@ -173,6 +173,10 @@ the life-time of their registered entities. - loadClassMetadata - The loadClassMetadata event occurs after the mapping metadata for a class has been loaded from a mapping source (annotations/xml/yaml). This event is not a lifecycle callback. +- onClassMetadataNotFound - Loading class metadata for a particular + requested class name failed. Manipulating the given event args instance + allows providing fallback metadata even when no actual metadata exists + or could be found. This event is not a lifecycle callback. - preFlush - The preFlush event occurs at the very beginning of a flush operation. This event is not a lifecycle callback. - onFlush - The onFlush event occurs after the change-sets of all diff --git a/lib/Doctrine/ORM/Event/OnClassMetadataNotFoundEventArgs.php b/lib/Doctrine/ORM/Event/OnClassMetadataNotFoundEventArgs.php new file mode 100644 index 00000000000..155b1baf671 --- /dev/null +++ b/lib/Doctrine/ORM/Event/OnClassMetadataNotFoundEventArgs.php @@ -0,0 +1,89 @@ +. + */ + +namespace Doctrine\ORM\Event; + +use Doctrine\Common\EventArgs; +use Doctrine\Common\Persistence\Event\ManagerEventArgs; +use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; + +/** + * Class that holds event arguments for a `onClassMetadataNotFound` event. + * + * This object is mutable by design, allowing callbacks having access to it to set the + * found metadata in it, and therefore "cancelling" a `onClassMetadataNotFound` event + * + * @author Marco Pivetta + * @since 2.5 + */ +class OnClassMetadataNotFoundEventArgs extends ManagerEventArgs +{ + /** + * @var string + */ + private $className; + + /** + * @var ClassMetadata|null + */ + private $foundMetadata; + + /** + * Constructor. + * + * @param string $className + * @param ObjectManager $objectManager + */ + public function __construct($className, ObjectManager $objectManager) + { + $this->className = (string) $className; + + parent::__construct($objectManager); + } + + /** + * @param ClassMetadata|null $classMetadata + */ + public function setFoundMetadata(ClassMetadata $classMetadata = null) + { + $this->foundMetadata = $classMetadata; + } + + /** + * @return ClassMetadata|null + */ + public function getFoundMetadata() + { + return $this->foundMetadata; + } + + /** + * Retrieve class name for which a failed metadata fetch attempt was executed + * + * @return string + */ + public function getClassName() + { + return $this->className; + } +} + diff --git a/lib/Doctrine/ORM/Events.php b/lib/Doctrine/ORM/Events.php index 8c13fa2d56a..e16b47a4214 100644 --- a/lib/Doctrine/ORM/Events.php +++ b/lib/Doctrine/ORM/Events.php @@ -120,6 +120,14 @@ private function __construct() */ const loadClassMetadata = 'loadClassMetadata'; + /** + * The onClassMetadataNotFound event occurs whenever loading metadata for a class + * failed. + * + * @var string + */ + const onClassMetadataNotFound = 'onClassMetadataNotFound'; + /** * The preFlush event occurs when the EntityManager#flush() operation is invoked, * but before any changes to managed entities have been calculated. This event is diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index 79e2fc8ded0..dd65c39323d 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -25,6 +25,7 @@ use Doctrine\DBAL\Platforms; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\LoadClassMetadataEventArgs; +use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs; use Doctrine\ORM\Events; use Doctrine\ORM\Id\BigIntegerIdentityGenerator; use Doctrine\ORM\Id\IdentityGenerator; @@ -78,7 +79,7 @@ public function setEntityManager(EntityManagerInterface $em) } /** - * {@inheritDoc}. + * {@inheritDoc} */ protected function initialize() { @@ -88,6 +89,22 @@ protected function initialize() $this->initialized = true; } + /** + * {@inheritDoc} + */ + protected function onNotFoundMetadata($className) + { + if (! $this->evm->hasListeners(Events::onClassMetadataNotFound)) { + return; + } + + $eventArgs = new OnClassMetadataNotFoundEventArgs($className, $this->em); + + $this->evm->dispatchEvent(Events::onClassMetadataNotFound, $eventArgs); + + return $eventArgs->getFoundMetadata(); + } + /** * {@inheritDoc} */ diff --git a/lib/Doctrine/ORM/Tools/ResolveTargetEntityListener.php b/lib/Doctrine/ORM/Tools/ResolveTargetEntityListener.php index e193ae2e3e7..574c9c28e12 100644 --- a/lib/Doctrine/ORM/Tools/ResolveTargetEntityListener.php +++ b/lib/Doctrine/ORM/Tools/ResolveTargetEntityListener.php @@ -20,7 +20,10 @@ namespace Doctrine\ORM\Tools; use Doctrine\ORM\Event\LoadClassMetadataEventArgs; +use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\Common\EventSubscriber; +use Doctrine\ORM\Events; /** * ResolveTargetEntityListener @@ -31,13 +34,24 @@ * @author Benjamin Eberlei * @since 2.2 */ -class ResolveTargetEntityListener +class ResolveTargetEntityListener implements EventSubscriber { /** - * @var array + * @var array[] indexed by original entity name */ private $resolveTargetEntities = array(); + /** + * {@inheritDoc} + */ + public function getSubscribedEvents() + { + return array( + Events::loadClassMetadata, + Events::onClassMetadataNotFound + ); + } + /** * Adds a target-entity class name to resolve to a new class name. * @@ -49,19 +63,40 @@ class ResolveTargetEntityListener */ public function addResolveTargetEntity($originalEntity, $newEntity, array $mapping) { - $mapping['targetEntity'] = ltrim($newEntity, "\\"); + $mapping['targetEntity'] = ltrim($newEntity, "\\"); $this->resolveTargetEntities[ltrim($originalEntity, "\\")] = $mapping; } + /** + * @param OnClassMetadataNotFoundEventArgs $args + * + * @internal this is an event callback, and should not be called directly + * + * @return void + */ + public function onClassMetadataNotFound(OnClassMetadataNotFoundEventArgs $args) + { + if (array_key_exists($args->getClassName(), $this->resolveTargetEntities)) { + $args->setFoundMetadata( + $args + ->getObjectManager() + ->getClassMetadata($this->resolveTargetEntities[$args->getClassname()]['targetEntity']) + ); + } + } + /** * Processes event and resolves new target entity names. * * @param LoadClassMetadataEventArgs $args * * @return void + * + * @internal this is an event callback, and should not be called directly */ public function loadClassMetadata(LoadClassMetadataEventArgs $args) { + /* @var $cm \Doctrine\ORM\Mapping\ClassMetadata */ $cm = $args->getClassMetadata(); foreach ($cm->associationMappings as $mapping) { @@ -69,6 +104,12 @@ public function loadClassMetadata(LoadClassMetadataEventArgs $args) $this->remapAssociation($cm, $mapping); } } + + foreach ($this->resolveTargetEntities as $interface => $data) { + if ($data['targetEntity'] == $cm->getName()) { + $args->getEntityManager()->getMetadataFactory()->setMetadataFor($interface, $cm); + } + } } /** diff --git a/tests/Doctrine/Tests/ORM/Event/OnClassMetadataNotFoundEventArgsTest.php b/tests/Doctrine/Tests/ORM/Event/OnClassMetadataNotFoundEventArgsTest.php new file mode 100644 index 00000000000..3aad1c04cc9 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Event/OnClassMetadataNotFoundEventArgsTest.php @@ -0,0 +1,38 @@ +getMock('Doctrine\Common\Persistence\ObjectManager'); + + $args = new OnClassMetadataNotFoundEventArgs('foo', $objectManager); + + $this->assertSame('foo', $args->getClassName()); + $this->assertSame($objectManager, $args->getObjectManager()); + + $this->assertNull($args->getFoundMetadata()); + + /* @var $metadata \Doctrine\Common\Persistence\Mapping\ClassMetadata */ + $metadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + + $args->setFoundMetadata($metadata); + + $this->assertSame($metadata, $args->getFoundMetadata()); + + $args->setFoundMetadata(null); + + $this->assertNull($args->getFoundMetadata()); + } +} diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataFactoryTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataFactoryTest.php index e87d4dc516d..54d2c9eac01 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataFactoryTest.php @@ -2,6 +2,8 @@ namespace Doctrine\Tests\ORM\Mapping; +use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs; +use Doctrine\ORM\Events; use Doctrine\Tests\Mocks\MetadataDriverMock; use Doctrine\Tests\Mocks\EntityManagerMock; use Doctrine\Tests\Mocks\ConnectionMock; @@ -322,6 +324,40 @@ public function testQuoteMetadata() $this->assertEquals('group-id', $groups['joinTable']['inverseJoinColumns'][0]['referencedColumnName']); } + /** + * @group DDC-3385 + * @group 1181 + * @group 385 + */ + public function testFallbackLoadingCausesEventTriggeringThatCanModifyFetchedMetadata() + { + $test = $this; + /* @var $metadata \Doctrine\Common\Persistence\Mapping\ClassMetadata */ + $metadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + $cmf = new ClassMetadataFactory(); + $mockDriver = new MetadataDriverMock(); + $em = $this->_createEntityManager($mockDriver); + $listener = $this->getMock('stdClass', array('onClassMetadataNotFound')); + $eventManager = $em->getEventManager(); + + $cmf->setEntityManager($em); + + $listener + ->expects($this->any()) + ->method('onClassMetadataNotFound') + ->will($this->returnCallback(function (OnClassMetadataNotFoundEventArgs $args) use ($metadata, $em, $test) { + $test->assertNull($args->getFoundMetadata()); + $test->assertSame('Foo', $args->getClassName()); + $test->assertSame($em, $args->getObjectManager()); + + $args->setFoundMetadata($metadata); + })); + + $eventManager->addEventListener(array(Events::onClassMetadataNotFound), $listener); + + $this->assertSame($metadata, $cmf->getMetadataFor('Foo')); + } + /** * @group DDC-3427 */ diff --git a/tests/Doctrine/Tests/ORM/Tools/ResolveTargetEntityListenerTest.php b/tests/Doctrine/Tests/ORM/Tools/ResolveTargetEntityListenerTest.php index 3b42196231e..fdee6e8c2f1 100644 --- a/tests/Doctrine/Tests/ORM/Tools/ResolveTargetEntityListenerTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/ResolveTargetEntityListenerTest.php @@ -2,9 +2,9 @@ namespace Doctrine\Tests\ORM\Tools; -use Doctrine\ORM\Events; use Doctrine\ORM\Mapping\ClassMetadataFactory; use Doctrine\ORM\Tools\ResolveTargetEntityListener; +use Doctrine\ORM\Events; class ResolveTargetEntityListenerTest extends \Doctrine\Tests\OrmTestCase { @@ -29,9 +29,8 @@ public function setUp() $this->em = $this->_getTestEntityManager(); $this->em->getConfiguration()->setMetadataDriverImpl($annotationDriver); - $this->factory = new ClassMetadataFactory; - $this->factory->setEntityManager($this->em); - $this->listener = new ResolveTargetEntityListener; + $this->factory = $this->em->getMetadataFactory(); + $this->listener = new ResolveTargetEntityListener(); } /** @@ -50,13 +49,37 @@ public function testResolveTargetEntityListenerCanResolveTargetEntity() 'Doctrine\Tests\ORM\Tools\TargetEntity', array() ); - $evm->addEventListener(Events::loadClassMetadata, $this->listener); - $cm = $this->factory->getMetadataFor('Doctrine\Tests\ORM\Tools\ResolveTargetEntity'); + $evm->addEventSubscriber($this->listener); + + $cm = $this->factory->getMetadataFor('Doctrine\Tests\ORM\Tools\ResolveTargetEntity'); $meta = $cm->associationMappings; + $this->assertSame('Doctrine\Tests\ORM\Tools\TargetEntity', $meta['manyToMany']['targetEntity']); $this->assertSame('Doctrine\Tests\ORM\Tools\ResolveTargetEntity', $meta['manyToOne']['targetEntity']); $this->assertSame('Doctrine\Tests\ORM\Tools\ResolveTargetEntity', $meta['oneToMany']['targetEntity']); $this->assertSame('Doctrine\Tests\ORM\Tools\TargetEntity', $meta['oneToOne']['targetEntity']); + + $this->assertSame($cm, $this->factory->getMetadataFor('Doctrine\Tests\ORM\Tools\ResolveTargetInterface')); + } + + /** + * @group DDC-3385 + * @group 1181 + * @group 385 + */ + public function testResolveTargetEntityListenerCanRetrieveTargetEntityByInterfaceName() + { + $this->listener->addResolveTargetEntity( + 'Doctrine\Tests\ORM\Tools\ResolveTargetInterface', + 'Doctrine\Tests\ORM\Tools\ResolveTargetEntity', + array() + ); + + $this->em->getEventManager()->addEventSubscriber($this->listener); + + $cm = $this->factory->getMetadataFor('Doctrine\Tests\ORM\Tools\ResolveTargetInterface'); + + $this->assertSame($this->factory->getMetadataFor('Doctrine\Tests\ORM\Tools\ResolveTargetEntity'), $cm); } /**