diff --git a/src/Symfony/Component/EventDispatcher/ContainerAwareEventDispatcher.php b/src/Symfony/Component/EventDispatcher/ContainerAwareEventDispatcher.php index e97d427ea15eb..f748410129412 100644 --- a/src/Symfony/Component/EventDispatcher/ContainerAwareEventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/ContainerAwareEventDispatcher.php @@ -12,6 +12,7 @@ namespace Symfony\Component\EventDispatcher; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\IntrospectableContainerInterface; /** * Lazily loads listeners and subscribers from the dependency injection @@ -30,16 +31,10 @@ class ContainerAwareEventDispatcher extends EventDispatcher private $container; /** - * The service IDs of the event listeners and subscribers - * @var array - */ - private $listenerIds = array(); - - /** - * The services registered as listeners - * @var array + * A list of proxy closures indexed by eventName, serviceId, method. + * @var ContainerInterface */ - private $listeners = array(); + private $proxies; /** * Constructor. @@ -69,66 +64,42 @@ public function addListenerService($eventName, $callback, $priority = 0) throw new \InvalidArgumentException('Expected an array("service", "method") argument'); } - $this->listenerIds[$eventName][] = array($callback[0], $callback[1], $priority); - } - - public function removeListener($eventName, $listener) - { - $this->lazyLoad($eventName); + $container = $this->container; + list($serviceId, $method) = $callback; - if (isset($this->listeners[$eventName])) { - foreach ($this->listeners[$eventName] as $key => $l) { - foreach ($this->listenerIds[$eventName] as $i => $args) { - list($serviceId, $method, $priority) = $args; - if ($key === $serviceId.'.'.$method) { - if ($listener === array($l, $method)) { - unset($this->listeners[$eventName][$key]); - if (empty($this->listeners[$eventName])) { - unset($this->listeners[$eventName]); - } - unset($this->listenerIds[$eventName][$i]); - if (empty($this->listenerIds[$eventName])) { - unset($this->listenerIds[$eventName]); - } - } - } - } - } + if (isset($this->proxies[$eventName][$serviceId][$method])) { + $proxy = $this->proxies[$eventName][$serviceId][$method]; + unset($this->proxies[$eventName][$serviceId][$method]); + parent::removeListener($eventName, $proxy); } - parent::removeListener($eventName, $listener); + $proxy = new LazyServiceListener($container, $serviceId, $method); + $this->proxies[$eventName][$serviceId][$method] = $proxy; + parent::addListener($eventName, $proxy, $priority); } /** - * @see EventDispatcherInterface::hasListeners + * @see EventDispatcherInterface::removeListener */ - public function hasListeners($eventName = null) - { - if (null === $eventName) { - return (bool) count($this->listenerIds) || (bool) count($this->listeners); - } - - if (isset($this->listenerIds[$eventName])) { - return true; - } - - return parent::hasListeners($eventName); - } - - /** - * @see EventDispatcherInterface::getListeners - */ - public function getListeners($eventName = null) + public function removeListener($eventName, $listener) { - if (null === $eventName) { - foreach (array_keys($this->listenerIds) as $serviceEventName) { - $this->lazyLoad($serviceEventName); + $introspect = $this->container instanceof IntrospectableContainerInterface; + if (isset($this->proxies[$eventName])) { + foreach ($this->proxies[$eventName] as $serviceId => $methods) { + if (!$introspect || $this->container->initialized($serviceId)) { + foreach ($methods as $method => $proxy) { + if ($listener === array($this->container->get($serviceId), $method)) { + unset($this->proxies[$eventName][$serviceId][$method]); + parent::removeListener($eventName, $proxy); + + return; + } + } + } } - } else { - $this->lazyLoad($eventName); } - return parent::getListeners($eventName); + parent::removeListener($eventName, $listener); } /** @@ -141,62 +112,19 @@ public function addSubscriberService($serviceId, $class) { foreach ($class::getSubscribedEvents() as $eventName => $params) { if (is_string($params)) { - $this->listenerIds[$eventName][] = array($serviceId, $params, 0); + $this->addListenerService($eventName, array($serviceId, $params), 0); } elseif (is_string($params[0])) { - $this->listenerIds[$eventName][] = array($serviceId, $params[0], isset($params[1]) ? $params[1] : 0); + $this->addListenerService($eventName, array($serviceId, $params[0]), isset($params[1]) ? $params[1] : 0); } else { foreach ($params as $listener) { - $this->listenerIds[$eventName][] = array($serviceId, $listener[0], isset($listener[1]) ? $listener[1] : 0); + $this->addListenerService($eventName, array($serviceId, $listener[0]), isset($listener[1]) ? $listener[1] : 0); } } } } - /** - * {@inheritdoc} - * - * Lazily loads listeners for this event from the dependency injection - * container. - * - * @throws \InvalidArgumentException if the service is not defined - */ - public function dispatch($eventName, Event $event = null) - { - $this->lazyLoad($eventName); - - return parent::dispatch($eventName, $event); - } - public function getContainer() { return $this->container; } - - /** - * Lazily loads listeners for this event from the dependency injection - * container. - * - * @param string $eventName The name of the event to dispatch. The name of - * the event is the name of the method that is - * invoked on listeners. - */ - protected function lazyLoad($eventName) - { - if (isset($this->listenerIds[$eventName])) { - foreach ($this->listenerIds[$eventName] as $args) { - list($serviceId, $method, $priority) = $args; - $listener = $this->container->get($serviceId); - - $key = $serviceId.'.'.$method; - if (!isset($this->listeners[$eventName][$key])) { - $this->addListener($eventName, array($listener, $method), $priority); - } elseif ($listener !== $this->listeners[$eventName][$key]) { - parent::removeListener($eventName, array($this->listeners[$eventName][$key], $method)); - $this->addListener($eventName, array($listener, $method), $priority); - } - - $this->listeners[$eventName][$key] = $listener; - } - } - } } diff --git a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php index b797667208bb5..8e4cd6b7ce8b7 100644 --- a/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php @@ -14,6 +14,7 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\Event; +use Symfony\Component\EventDispatcher\LazyServiceListener; use Symfony\Component\Stopwatch\Stopwatch; use Psr\Log\LoggerInterface; @@ -268,6 +269,13 @@ private function getListenerInfo($listener, $eventName) $info = array( 'event' => $eventName, ); + // Unpack lazy container service listener. + if ($listener instanceof LazyServiceListener) { + $container = $listener->getContainer(); + $method = $listener->getMethod(); + $serviceId = $listener->getServiceId(); + $listener = array($container->get($serviceId), $method); + } if ($listener instanceof \Closure) { $info += array( 'type' => 'Closure', diff --git a/src/Symfony/Component/EventDispatcher/LazyServiceListener.php b/src/Symfony/Component/EventDispatcher/LazyServiceListener.php new file mode 100644 index 0000000000000..f8a557e606b3f --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/LazyServiceListener.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * A listener forwarding its invocation to a service. + */ +class LazyServiceListener +{ + /** + * The container from where service is loaded + * @var ContainerInterface + */ + private $container; + + /** + * The service id. + * @var string + */ + private $serviceId; + + /** + * The name of a method on the service. + * @var string + */ + private $method; + + /** + * Constructor. + * + * @param ContainerInterface $container The service container + * @param string $serviceId The service identifier + * @param string $method The method name + */ + public function __construct(ContainerInterface $container, $serviceId, $method) + { + $this->container = $container; + $this->serviceId = $serviceId; + $this->method = $method; + } + + /** + * Retrieves the service from the container and forwards the method call. + */ + public function __invoke(Event $event, $eventName, EventDispatcherInterface $dispatcher) + { + $service = $this->container->get($this->serviceId); + $service->{$this->method}($event, $eventName, $dispatcher); + } + + /** + * Returns the container. + */ + public function getContainer() + { + return $this->container; + } + + /** + * Returns the service id. + */ + public function getServiceId() + { + return $this->serviceId; + } + + /** + * Returns the method name. + */ + public function getMethod() + { + return $this->method; + } +} diff --git a/src/Symfony/Component/EventDispatcher/Tests/ContainerAwareEventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/ContainerAwareEventDispatcherTest.php index fb3b4caa26624..4478a6970ef80 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/ContainerAwareEventDispatcherTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/ContainerAwareEventDispatcherTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\EventDispatcher\Tests; use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Scope; use Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher; use Symfony\Component\EventDispatcher\Event; @@ -28,8 +30,7 @@ public function testAddAListenerService() $service ->expects($this->once()) ->method('onEvent') - ->with($event) - ; + ->with($event); $container = new Container(); $container->set('service.listener', $service); @@ -49,8 +50,7 @@ public function testAddASubscriberService() $service ->expects($this->once()) ->method('onEvent') - ->with($event) - ; + ->with($event); $container = new Container(); $container->set('service.subscriber', $service); @@ -70,8 +70,7 @@ public function testPreventDuplicateListenerService() $service ->expects($this->once()) ->method('onEvent') - ->with($event) - ; + ->with($event); $container = new Container(); $container->set('service.listener', $service); @@ -113,8 +112,7 @@ public function testReEnteringAScope() $service1 ->expects($this->exactly(2)) ->method('onEvent') - ->with($event) - ; + ->with($event); $scope = new Scope('scope'); $container = new Container(); @@ -132,8 +130,7 @@ public function testReEnteringAScope() $service2 ->expects($this->once()) ->method('onEvent') - ->with($event) - ; + ->with($event); $container->enterScope('scope'); $container->set('service.listener', $service2, 'scope'); @@ -163,8 +160,7 @@ public function testHasListenersOnLazyLoad() $service ->expects($this->once()) ->method('onEvent') - ->with($event) - ; + ->with($event); $this->assertTrue($dispatcher->hasListeners()); @@ -218,6 +214,48 @@ public function testRemoveBeforeDispatch() $dispatcher->removeListener('onEvent', array($container->get('service.listener'), 'onEvent')); $this->assertFalse($dispatcher->hasListeners('onEvent')); } + + public function testLazyInstantiation() + { + $event = new Event(); + + $non_propagating_service = $this->getMock('Symfony\Component\EventDispatcher\Tests\Service'); + + $non_propagating_service + ->expects($this->once()) + ->method('onEvent') + ->with($event) + ->will($this->returnCallback(function ($event) { + $event->stopPropagation(); + })); + + $container = new ContainerBuilder(); + $container->set('service.listener', $non_propagating_service); + $container->setDefinition('service.listener.omitted', new Definition('Symfony\Component\EventDispatcher\Tests\Service')); + + $dispatcher = new ContainerAwareEventDispatcher($container); + $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent')); + $dispatcher->addListenerService('onEvent', array('service.listener.omitted', 'onEvent')); + + $dispatcher->dispatch('onEvent', $event); + + $this->assertFalse($container->initialized('service.listener.omitted')); + } + + public function testRemoveLazyInstantiation() + { + $container = new ContainerBuilder(); + $container->setDefinition('service.listener', new Definition('Symfony\Component\EventDispatcher\Tests\Service')); + $container->setDefinition('service.listener.second', new Definition('Symfony\Component\EventDispatcher\Tests\Service')); + + $dispatcher = new ContainerAwareEventDispatcher($container); + $dispatcher->addListenerService('onEvent', array('service.listener', 'onEvent')); + $this->assertFalse($container->initialized('service.listener')); + + $dispatcher->removeListener('onEvent', array($container->get('service.listener'), 'onEvent')); + $this->assertFalse($dispatcher->hasListeners('onEvent')); + $this->assertFalse($container->initialized('service.listener.second')); + } } class Service