diff --git a/DependencyInjection/Compiler/EntityListenerPass.php b/DependencyInjection/Compiler/EntityListenerPass.php index ab0cc00d2..f1eff8fe5 100644 --- a/DependencyInjection/Compiler/EntityListenerPass.php +++ b/DependencyInjection/Compiler/EntityListenerPass.php @@ -2,8 +2,12 @@ namespace Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler; +use Doctrine\Bundle\DoctrineBundle\Mapping\ContainerEntityListenerResolver; +use Doctrine\Bundle\DoctrineBundle\Mapping\EntityListenerServiceResolver; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; @@ -19,6 +23,8 @@ public function process(ContainerBuilder $container) { $resolvers = $container->findTaggedServiceIds('doctrine.orm.entity_listener'); + $lazyServiceReferencesByResolver = []; + foreach ($resolvers as $id => $tagAttributes) { foreach ($tagAttributes as $attributes) { $name = isset($attributes['entity_manager']) ? $attributes['entity_manager'] : $container->getParameter('doctrine.default_entity_manager'); @@ -41,35 +47,44 @@ public function process(ContainerBuilder $container) $this->attachToListener($container, $name, $id, $attributes); } - if (isset($attributes['lazy']) && $attributes['lazy']) { + $resolverClass = $this->getResolverClass($resolver, $container); + $resolverSupportsLazyListeners = is_a($resolverClass, EntityListenerServiceResolver::class, true); + + $lazyByAttribute = isset($attributes['lazy']) && $attributes['lazy']; + if ($lazyByAttribute && ! $resolverSupportsLazyListeners) { + throw new InvalidArgumentException(sprintf( + 'Lazy-loaded entity listeners can only be resolved by a resolver implementing %s.', + EntityListenerServiceResolver::class + )); + } + + if (! isset($attributes['lazy']) && $resolverSupportsLazyListeners || $lazyByAttribute) { $listener = $container->findDefinition($id); if ($listener->isAbstract()) { throw new InvalidArgumentException(sprintf('The service "%s" must not be abstract as this entity listener is lazy-loaded.', $id)); } - $interface = 'Doctrine\\Bundle\\DoctrineBundle\\Mapping\\EntityListenerServiceResolver'; - $class = $resolver->getClass(); - - if (substr($class, 0, 1) === '%') { - // resolve container parameter first - $class = $container->getParameterBag()->resolveValue($resolver->getClass()); - } + $resolver->addMethodCall('registerService', [$listener->getClass(), $id]); - if (! is_a($class, $interface, true)) { - throw new InvalidArgumentException( - sprintf('Lazy-loaded entity listeners can only be resolved by a resolver implementing %s.', $interface) - ); + // if the resolver uses the default class we will use a service locator for all listeners + if ($resolverClass === ContainerEntityListenerResolver::class) { + if (! isset($lazyServiceReferencesByResolver[$resolverId])) { + $lazyServiceReferencesByResolver[$resolverId] = []; + } + $lazyServiceReferencesByResolver[$resolverId][$id] = new Reference($id); + } else { + $listener->setPublic(true); } - - $listener->setPublic(true); - - $resolver->addMethodCall('registerService', [$listener->getClass(), $id]); } else { $resolver->addMethodCall('register', [new Reference($id)]); } } } + + foreach ($lazyServiceReferencesByResolver as $resolverId => $listenerReferences) { + $container->findDefinition($resolverId)->setArgument(0, ServiceLocatorTagPass::register($container, $listenerReferences)); + } } private function attachToListener(ContainerBuilder $container, $name, $id, array $attributes) @@ -94,4 +109,16 @@ private function attachToListener(ContainerBuilder $container, $name, $id, array $container->findDefinition($listenerId)->addMethodCall('addEntityListener', $args); } + + private function getResolverClass(Definition $resolver, ContainerBuilder $container) : string + { + $resolverClass = $resolver->getClass(); + + if (substr($resolverClass, 0, 1) === '%') { + // resolve container parameter first + $resolverClass = $container->getParameterBag()->resolveValue($resolver->getClass()); + } + + return $resolverClass; + } } diff --git a/Mapping/ContainerAwareEntityListenerResolver.php b/Mapping/ContainerAwareEntityListenerResolver.php index 4345015b7..89f8320d7 100644 --- a/Mapping/ContainerAwareEntityListenerResolver.php +++ b/Mapping/ContainerAwareEntityListenerResolver.php @@ -2,107 +2,9 @@ namespace Doctrine\Bundle\DoctrineBundle\Mapping; -use InvalidArgumentException; -use RuntimeException; -use Symfony\Component\DependencyInjection\ContainerInterface; - -class ContainerAwareEntityListenerResolver implements EntityListenerServiceResolver +/** + * @deprecated since DoctrineBundle 1.11 and will be removed in 2.0. Use ContainerEntityListenerResolver instead. + */ +class ContainerAwareEntityListenerResolver extends ContainerEntityListenerResolver { - /** @var ContainerInterface */ - private $container; - - /** @var object[] Map to store entity listener instances. */ - private $instances = []; - - /** @var string[] Map to store registered service ids */ - private $serviceIds = []; - - public function __construct(ContainerInterface $container) - { - $this->container = $container; - } - - /** - * {@inheritdoc} - */ - public function clear($className = null) - { - if ($className === null) { - $this->instances = []; - - return; - } - - $className = $this->normalizeClassName($className); - - if (! isset($this->instances[$className])) { - return; - } - - unset($this->instances[$className]); - } - - /** - * {@inheritdoc} - */ - public function register($object) - { - if (! is_object($object)) { - throw new InvalidArgumentException(sprintf('An object was expected, but got "%s".', gettype($object))); - } - - $className = $this->normalizeClassName(get_class($object)); - - $this->instances[$className] = $object; - } - - /** - * {@inheritdoc} - */ - public function registerService($className, $serviceId) - { - $this->serviceIds[$this->normalizeClassName($className)] = $serviceId; - } - - /** - * {@inheritdoc} - */ - public function resolve($className) - { - $className = $this->normalizeClassName($className); - - if (! isset($this->instances[$className])) { - if (isset($this->serviceIds[$className])) { - $this->instances[$className] = $this->resolveService($this->serviceIds[$className]); - } else { - $this->instances[$className] = new $className(); - } - } - - return $this->instances[$className]; - } - - /** - * @param string $serviceId - * - * @return object - */ - private function resolveService($serviceId) - { - if (! $this->container->has($serviceId)) { - throw new RuntimeException(sprintf('There is no service named "%s"', $serviceId)); - } - - return $this->container->get($serviceId); - } - - /** - * @param string $className - * - * @return string - */ - private function normalizeClassName($className) - { - return trim($className, '\\'); - } } diff --git a/Mapping/ContainerEntityListenerResolver.php b/Mapping/ContainerEntityListenerResolver.php new file mode 100644 index 000000000..9645838d8 --- /dev/null +++ b/Mapping/ContainerEntityListenerResolver.php @@ -0,0 +1,103 @@ +container = $container; + } + + /** + * {@inheritdoc} + */ + public function clear($className = null) + { + if ($className === null) { + $this->instances = []; + + return; + } + + $className = $this->normalizeClassName($className); + + unset($this->instances[$className]); + } + + /** + * {@inheritdoc} + */ + public function register($object) + { + if (! is_object($object)) { + throw new InvalidArgumentException(sprintf('An object was expected, but got "%s".', gettype($object))); + } + + $className = $this->normalizeClassName(get_class($object)); + + $this->instances[$className] = $object; + } + + /** + * {@inheritdoc} + */ + public function registerService($className, $serviceId) + { + $this->serviceIds[$this->normalizeClassName($className)] = $serviceId; + } + + /** + * {@inheritdoc} + */ + public function resolve($className) + { + $className = $this->normalizeClassName($className); + + if (! isset($this->instances[$className])) { + if (isset($this->serviceIds[$className])) { + $this->instances[$className] = $this->resolveService($this->serviceIds[$className]); + } else { + $this->instances[$className] = new $className(); + } + } + + return $this->instances[$className]; + } + + /** + * @return object + */ + private function resolveService(string $serviceId) + { + if (! $this->container->has($serviceId)) { + throw new RuntimeException(sprintf('There is no service named "%s"', $serviceId)); + } + + return $this->container->get($serviceId); + } + + private function normalizeClassName(string $className) : string + { + return trim($className, '\\'); + } +} diff --git a/Resources/config/orm.xml b/Resources/config/orm.xml index 0fe60c165..ab9dd5bbe 100644 --- a/Resources/config/orm.xml +++ b/Resources/config/orm.xml @@ -62,7 +62,7 @@ Doctrine\ORM\Mapping\AnsiQuoteStrategy - Doctrine\Bundle\DoctrineBundle\Mapping\ContainerAwareEntityListenerResolver + Doctrine\Bundle\DoctrineBundle\Mapping\ContainerEntityListenerResolver Doctrine\ORM\Cache\DefaultCacheFactory diff --git a/Tests/DependencyInjection/AbstractDoctrineExtensionTest.php b/Tests/DependencyInjection/AbstractDoctrineExtensionTest.php index 6f8793df4..a26437388 100644 --- a/Tests/DependencyInjection/AbstractDoctrineExtensionTest.php +++ b/Tests/DependencyInjection/AbstractDoctrineExtensionTest.php @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ServiceLocator; abstract class AbstractDoctrineExtensionTest extends TestCase { @@ -831,7 +832,7 @@ public function testEntityListenerResolver() $this->assertDICDefinitionMethodCallOnce($definition, 'setEntityListenerResolver', [new Reference('doctrine.orm.em2_entity_listener_resolver')]); $listener = $container->getDefinition('doctrine.orm.em1_entity_listener_resolver'); - $this->assertDICDefinitionMethodCallOnce($listener, 'register', [new Reference('entity_listener1')]); + $this->assertDICDefinitionMethodCallOnce($listener, 'registerService', ['EntityListener', 'entity_listener1']); $listener = $container->getDefinition('entity_listener_resolver'); $this->assertDICDefinitionMethodCallOnce($listener, 'register', [new Reference('entity_listener2')]); @@ -849,10 +850,10 @@ public function testAttachEntityListenerTag() $this->compileContainer($container); $listener = $container->getDefinition('doctrine.orm.em1_entity_listener_resolver'); - $this->assertDICDefinitionMethodCallOnce($listener, 'register', [new Reference('entity_listener1')]); + $this->assertDICDefinitionMethodCallOnce($listener, 'registerService', ['EntityListener1', 'entity_listener1']); $listener = $container->getDefinition('doctrine.orm.em2_entity_listener_resolver'); - $this->assertDICDefinitionMethodCallOnce($listener, 'register', [new Reference('entity_listener2')]); + $this->assertDICDefinitionMethodCallOnce($listener, 'registerService', ['EntityListener2', 'entity_listener2']); $attachListener = $container->getDefinition('doctrine.orm.em1_listeners.attach_entity_listeners'); $this->assertDICDefinitionMethodCallOnce($attachListener, 'addEntityListener', ['My/Entity1', 'EntityListener1', 'postLoad']); @@ -893,12 +894,39 @@ public function testAttachLazyEntityListener() $this->compileContainer($container); $resolver1 = $container->getDefinition('doctrine.orm.em1_entity_listener_resolver'); - $this->assertDICDefinitionMethodCallOnce($resolver1, 'registerService', ['EntityListener1', 'entity_listener1']); + $this->assertDICDefinitionMethodCallAt(0, $resolver1, 'registerService', ['EntityListener1', 'entity_listener1']); + $this->assertDICDefinitionMethodCallAt(1, $resolver1, 'register', [new Reference('entity_listener3')]); + $this->assertDICDefinitionMethodCallAt(2, $resolver1, 'registerService', ['EntityListener4', 'entity_listener4']); + + $serviceLocatorReference = $resolver1->getArgument(0); + $this->assertInstanceOf(Reference::class, $serviceLocatorReference); + $serviceLocatorDefinition = $container->getDefinition((string) $serviceLocatorReference); + $this->assertSame(ServiceLocator::class, $serviceLocatorDefinition->getClass()); + $serviceLocatorMap = $serviceLocatorDefinition->getArgument(0); + $this->assertSame(['entity_listener1', 'entity_listener4'], array_keys($serviceLocatorMap)); $resolver2 = $container->findDefinition('custom_entity_listener_resolver'); $this->assertDICDefinitionMethodCallOnce($resolver2, 'registerService', ['EntityListener2', 'entity_listener2']); } + public function testAttachLazyEntityListenerForCustomResolver() + { + $container = $this->getContainer([]); + $loader = new DoctrineExtension(); + $container->registerExtension($loader); + $container->addCompilerPass(new EntityListenerPass()); + + $this->loadFromFile($container, 'orm_entity_listener_custom_resolver'); + + $this->compileContainer($container); + + $resolver = $container->getDefinition('custom_entity_listener_resolver'); + $this->assertTrue($resolver->isPublic()); + $this->assertEmpty($resolver->getArguments(), 'We must not change the arguments for custom services.'); + $this->assertDICDefinitionMethodCallOnce($resolver, 'registerService', ['EntityListener', 'entity_listener']); + $this->assertTrue($container->getDefinition('entity_listener')->isPublic()); + } + /** * @expectedException \InvalidArgumentException * @expectedExceptionMessage EntityListenerServiceResolver @@ -1007,6 +1035,8 @@ private function assertDICDefinitionMethodCallAt($pos, Definition $definition, $ { $calls = $definition->getMethodCalls(); if (! isset($calls[$pos][0])) { + $this->fail(sprintf('Method call at position %s not found!', $pos)); + return; } diff --git a/Tests/DependencyInjection/Fixtures/CustomEntityListenerServiceResolver.php b/Tests/DependencyInjection/Fixtures/CustomEntityListenerServiceResolver.php new file mode 100644 index 000000000..76a43a279 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/CustomEntityListenerServiceResolver.php @@ -0,0 +1,48 @@ +resolver = $resolver; + } + + /** + * {@inheritdoc} + */ + public function clear($className = null) + { + $this->resolver->clear($className); + } + + /** + * {@inheritdoc} + */ + public function resolve($className) + { + return $this->resolver->resolve($className); + } + + /** + * {@inheritdoc} + */ + public function register($object) + { + $this->resolver->register($object); + } + + /** + * {@inheritdoc} + */ + public function registerService($className, $serviceId) + { + $this->resolver->registerService($className, $serviceId); + } +} diff --git a/Tests/DependencyInjection/Fixtures/config/xml/orm_attach_lazy_entity_listener.xml b/Tests/DependencyInjection/Fixtures/config/xml/orm_attach_lazy_entity_listener.xml index 65358d08f..1da2f822d 100644 --- a/Tests/DependencyInjection/Fixtures/config/xml/orm_attach_lazy_entity_listener.xml +++ b/Tests/DependencyInjection/Fixtures/config/xml/orm_attach_lazy_entity_listener.xml @@ -7,7 +7,7 @@ http://symfony.com/schema/dic/doctrine http://symfony.com/schema/dic/doctrine/doctrine-1.0.xsd"> - + @@ -17,6 +17,12 @@ + + + + + + diff --git a/Tests/DependencyInjection/Fixtures/config/xml/orm_entity_listener_custom_resolver.xml b/Tests/DependencyInjection/Fixtures/config/xml/orm_entity_listener_custom_resolver.xml new file mode 100644 index 000000000..69734f4e4 --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/config/xml/orm_entity_listener_custom_resolver.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/DependencyInjection/Fixtures/config/yml/orm_attach_lazy_entity_listener.yml b/Tests/DependencyInjection/Fixtures/config/yml/orm_attach_lazy_entity_listener.yml index 530186187..f312827e9 100644 --- a/Tests/DependencyInjection/Fixtures/config/yml/orm_attach_lazy_entity_listener.yml +++ b/Tests/DependencyInjection/Fixtures/config/yml/orm_attach_lazy_entity_listener.yml @@ -1,6 +1,6 @@ services: custom_entity_listener_resolver: - class: Doctrine\Bundle\DoctrineBundle\Mapping\ContainerAwareEntityListenerResolver + class: Doctrine\Bundle\DoctrineBundle\Mapping\ContainerEntityListenerResolver arguments: - '@service_container' @@ -14,6 +14,16 @@ services: tags: - { name: doctrine.orm.entity_listener, entity_manager: em2, lazy: true } + entity_listener3: + class: EntityListener3 + tags: + - { name: doctrine.orm.entity_listener, lazy: false } + + entity_listener4: + class: EntityListener4 + tags: + - { name: doctrine.orm.entity_listener } + doctrine: dbal: default_connection: default diff --git a/Tests/DependencyInjection/Fixtures/config/yml/orm_entity_listener_custom_resolver.yml b/Tests/DependencyInjection/Fixtures/config/yml/orm_entity_listener_custom_resolver.yml new file mode 100644 index 000000000..32c705feb --- /dev/null +++ b/Tests/DependencyInjection/Fixtures/config/yml/orm_entity_listener_custom_resolver.yml @@ -0,0 +1,23 @@ +services: + custom_entity_listener_resolver: + class: Doctrine\Bundle\DoctrineBundle\Tests\DependencyInjection\Fixtures\CustomEntityListenerServiceResolver + public: false + + entity_listener: + class: EntityListener + public: false + tags: + - { name: doctrine.orm.entity_listener } + +doctrine: + dbal: + default_connection: default + connections: + default: + dbname: db + + orm: + default_entity_manager: em1 + entity_managers: + em1: + entity_listener_resolver: custom_entity_listener_resolver diff --git a/Tests/DependencyInjection/Fixtures/config/yml/orm_entity_listener_resolver.yml b/Tests/DependencyInjection/Fixtures/config/yml/orm_entity_listener_resolver.yml index 2d93aeeae..bab9df4a2 100644 --- a/Tests/DependencyInjection/Fixtures/config/yml/orm_entity_listener_resolver.yml +++ b/Tests/DependencyInjection/Fixtures/config/yml/orm_entity_listener_resolver.yml @@ -1,14 +1,14 @@ services: entity_listener_resolver: - class: \Doctrine\ORM\Mapping\DefaultEntityListenerResolver + class: Doctrine\ORM\Mapping\DefaultEntityListenerResolver entity_listener1: - class: \EntityListener + class: EntityListener tags: - { name: doctrine.orm.entity_listener } entity_listener2: - class: \EntityListener + class: EntityListener tags: - { name: doctrine.orm.entity_listener, entity_manager: em2 } diff --git a/Tests/Mapping/ContainerAwareEntityListenerResolverTest.php b/Tests/Mapping/ContainerEntityListenerResolverTest.php similarity index 90% rename from Tests/Mapping/ContainerAwareEntityListenerResolverTest.php rename to Tests/Mapping/ContainerEntityListenerResolverTest.php index 696c77419..f6fa7bb7c 100644 --- a/Tests/Mapping/ContainerAwareEntityListenerResolverTest.php +++ b/Tests/Mapping/ContainerEntityListenerResolverTest.php @@ -2,14 +2,14 @@ namespace Doctrine\Bundle\DoctrineBundle\Tests\Mapping; -use Doctrine\Bundle\DoctrineBundle\Mapping\ContainerAwareEntityListenerResolver; +use Doctrine\Bundle\DoctrineBundle\Mapping\ContainerEntityListenerResolver; use PHPUnit\Framework\TestCase; use PHPUnit_Framework_MockObject_MockObject; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Psr\Container\ContainerInterface; -class ContainerAwareEntityListenerResolverTest extends TestCase +class ContainerEntityListenerResolverTest extends TestCase { - /** @var ContainerAwareEntityListenerResolver */ + /** @var ContainerEntityListenerResolver */ private $resolver; /** @var ContainerInterface|PHPUnit_Framework_MockObject_MockObject */ @@ -19,8 +19,8 @@ protected function setUp() { parent::setUp(); - $this->container = $this->getMockForAbstractClass('\Symfony\Component\DependencyInjection\ContainerInterface'); - $this->resolver = new ContainerAwareEntityListenerResolver($this->container); + $this->container = $this->createMock(ContainerInterface::class); + $this->resolver = new ContainerEntityListenerResolver($this->container); } public function testResolveClass()