From ec5e9046afb8c13d47b8b8f3f0f301bf3ce39d19 Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Wed, 19 Nov 2014 21:42:14 +0100 Subject: [PATCH 01/18] Add Drupal EventDispatcher --- .../DrupalRegisterListenersPass.php | 105 ++++++++ .../EventDispatcher/DrupalEventDispatcher.php | 247 ++++++++++++++++++ 2 files changed, 352 insertions(+) create mode 100644 src/Symfony/Component/EventDispatcher/DependencyInjection/DrupalRegisterListenersPass.php create mode 100644 src/Symfony/Component/EventDispatcher/DrupalEventDispatcher.php diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/DrupalRegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/DrupalRegisterListenersPass.php new file mode 100644 index 0000000000000..22c6a05eff407 --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/DrupalRegisterListenersPass.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Drupal\Core\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +/** + * Compiler pass to register tagged services for an event dispatcher. + */ +class DrupalRegisterListenersPass implements CompilerPassInterface +{ + /** + * @var string + */ + protected $dispatcherService; + + /** + * @var string + */ + protected $listenerTag; + + /** + * @var string + */ + protected $subscriberTag; + + /** + * Constructor. + * + * @param string $dispatcherService Service name of the event dispatcher in processed container + * @param string $listenerTag Tag name used for listener + * @param string $subscriberTag Tag name used for subscribers + */ + public function __construct($dispatcherService = 'event_dispatcher', $listenerTag = 'kernel.event_listener', $subscriberTag = 'kernel.event_subscriber') + { + $this->dispatcherService = $dispatcherService; + $this->listenerTag = $listenerTag; + $this->subscriberTag = $subscriberTag; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition($this->dispatcherService) && !$container->hasAlias($this->dispatcherService)) { + return; + } + + $definition = $container->findDefinition($this->dispatcherService); + + // TODO: Also collect services tagged as listeners. + + $event_subscriber_info = array(); + foreach ($container->findTaggedServiceIds($this->subscriberTag) as $id => $attributes) { + $def = $container->getDefinition($id); + if (!$def->isPublic()) { + throw new \InvalidArgumentException(sprintf('The service "%s" must be public as event listeners are lazy-loaded.', $id)); + } + + // We must assume that the class value has been correctly filled, even if the service is created by a factory + $class = $def->getClass(); + + $refClass = new \ReflectionClass($class); + $interface = 'Symfony\Component\EventDispatcher\EventSubscriberInterface'; + if (!$refClass->implementsInterface($interface)) { + throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface)); + } + + // Get all subscribed events. + foreach ($class::getSubscribedEvents() as $event_name => $params) { + if (is_string($params)) { + $priority = 0; + $event_subscriber_info[$event_name][$priority][] = array('service' => array($id, $params)); + } + elseif (is_string($params[0])) { + $priority = isset($params[1]) ? $params[1] : 0; + $event_subscriber_info[$event_name][$priority][] = array('service' => array($id, $params[0])); + } + else { + foreach ($params as $listener) { + $priority = isset($listener[1]) ? $listener[1] : 0; + $event_subscriber_info[$event_name][$priority][] = array('service' => array($id, $listener[0])); + } + } + } + } + + foreach (array_keys($event_subscriber_info) as $event_name) { + krsort($event_subscriber_info[$event_name]); + } + + $definition->addArgument($event_subscriber_info); + } +} diff --git a/src/Symfony/Component/EventDispatcher/DrupalEventDispatcher.php b/src/Symfony/Component/EventDispatcher/DrupalEventDispatcher.php new file mode 100644 index 0000000000000..58efeba24a03a --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/DrupalEventDispatcher.php @@ -0,0 +1,247 @@ + + * + * 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\IntrospectableContainerInterface; + +/** + * A performance optimized container aware event dispatcher. + * + * This version of the event dispatcher contains the following optimizations + * in comparison to the Symfony event dispatcher component: + * + *
+ *
Faster instantiation of the event dispatcher service
+ *
+ * Instead of calling addSubscriberService once for each + * subscriber, a precompiled array of listener definitions is passed + * directly to the constructor. This is faster by roughly an order of + * magnitude. The listeners are collected and prepared using a compiler + * pass. + *
+ *
Lazy instantiation of listeners
+ *
+ * Services are only retrieved from the container just before invocation. + * Especially when dispatching the KernelEvents::REQUEST event, this leads + * to a more timely invocation of the first listener. Overall dispatch + * runtime is not affected by this change though. + *
+ *
+ */ +class ContainerAwareEventDispatcher implements EventDispatcherInterface +{ + + /** + * The service container. + * + * @var \Symfony\Component\DependencyInjection\IntrospectableContainerInterface + */ + private $container; + + /** + * Listener definitions. + * + * A nested array of listener definitions keyed by event name and priority. + * A listener definition is an associative array with one of the following key + * value pairs: + * - callable: A callable listener + * - service: An array of the form array(service id, method) + * + * A service entry will be resolved to a callable only just before its + * invocation. + * + * @var array + */ + private $listeners; + + /** + * Whether listeners need to be sorted prior to dispatch, keyed by event name. + * + * @var TRUE[] + */ + private $unsorted; + + /** + * Constructs a container aware event dispatcher. + * + * @param \Symfony\Component\EventDispatcher\IntrospectableContainerInterface $container + * The service container. + * @param array $listeners + * A nested array of listener definitions keyed by event name and priority. + * The array is expected to be ordered by priority. A listener definition is + * an associative array with one of the following key value pairs: + * - callable: A callable listener + * - service: An array of the form array(service id, method) + * A service entry will be resolved to a callable only just before its + * invocation. + */ + public function __construct(IntrospectableContainerInterface $container, array $listeners = array()) + { + $this->container = $container; + $this->listeners = $listeners; + $this->unsorted = array(); + } + + /** + * {@inheritdoc} + */ + public function dispatch($event_name, Event $event = NULL) + { + if ($event === NULL) { + $event = new Event(); + } + + $event->setDispatcher($this); + $event->setName($event_name); + + if (isset($this->listeners[$event_name])) { + // Sort listeners if necessary. + if (isset($this->unsorted[$event_name])) { + krsort($this->listeners[$event_name]); + unset($this->unsorted[$event_name]); + } + + // Invoke listeners and resolve callables if necessary. + foreach ($this->listeners[$event_name] as $priority => &$definitions) { + foreach ($definitions as $key => &$definition) { + if (!isset($definition['callable'])) { + $definition['callable'] = array($this->container->get($definition['service'][0]), $definition['service'][1]); + } + + $definition['callable']($event, $event_name, $this); + if ($event->isPropagationStopped()) { + return $event; + } + } + } + } + + return $event; + } + + /** + * {@inheritdoc} + */ + public function getListeners($event_name = NULL) + { + $result = array(); + + if ($event_name === NULL) { + // If event name was omitted, collect all listeners of all events. + foreach (array_keys($this->listeners) as $event_name) { + $listeners = $this->getListeners($event_name); + if (!empty($listeners)) { + $result[$event_name] = $listeners; + } + } + } + elseif (isset($this->listeners[$event_name])) { + // Sort listeners if necessary. + if (isset($this->unsorted[$event_name])) { + krsort($this->listeners[$event_name]); + unset($this->unsorted[$event_name]); + } + + // Collect listeners and resolve callables if necessary. + foreach ($this->listeners[$event_name] as $priority => &$definitions) { + foreach ($definitions as $key => &$definition) { + if (!isset($definition['callable'])) { + $definition['callable'] = array($this->container->get($definition['service'][0]), $definition['service'][1]); + } + + $result[] = $definition['callable']; + } + } + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function hasListeners($event_name = NULL) + { + return (bool) count($this->getListeners($event_name)); + } + + /** + * {@inheritdoc} + */ + public function addListener($event_name, $listener, $priority = 0) + { + $this->listeners[$event_name][$priority][] = array('callable' => $listener); + $this->unsorted[$event_name] = TRUE; + } + + /** + * {@inheritdoc} + */ + public function removeListener($event_name, $listener) + { + if (!isset($this->listeners[$event_name])) { + return; + } + + foreach ($this->listeners[$event_name] as $priority => $definitions) { + foreach ($definitions as $key => $definition) { + if (!isset($definition['callable'])) { + if (!$this->container->initialized($definition['service'][0])) { + continue; + } + $definition['callable'] = array($this->container->get($definition['service'][0]), $definition['service'][1]); + } + + if ($definition['callable'] === $listener) { + unset($this->listeners[$event_name][$priority][$key]); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + foreach ($subscriber->getSubscribedEvents() as $event_name => $params) { + if (is_string($params)) { + $this->addListener($event_name, array($subscriber, $params)); + } + elseif (is_string($params[0])) { + $this->addListener($event_name, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0); + } + else { + foreach ($params as $listener) { + $this->addListener($event_name, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function removeSubscriber(EventSubscriberInterface $subscriber) + { + foreach ($subscriber->getSubscribedEvents() as $event_name => $params) { + if (is_array($params) && is_array($params[0])) { + foreach ($params as $listener) { + $this->removeListener($event_name, array($subscriber, $listener[0])); + } + } + else { + $this->removeListener($event_name, array($subscriber, is_string($params) ? $params : $params[0])); + } + } + } +} From 84c54d8a63ad5485b143d4e83c076f46ee4d7f41 Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Thu, 20 Nov 2014 12:36:45 +0100 Subject: [PATCH 02/18] Fix namespace, apply patch from CS fixer tool --- .../DrupalRegisterListenersPass.php | 8 +++---- .../EventDispatcher/DrupalEventDispatcher.php | 23 ++++++++----------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/DrupalRegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/DrupalRegisterListenersPass.php index 22c6a05eff407..a664432a94ef7 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/DrupalRegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/DrupalRegisterListenersPass.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Drupal\Core\DependencyInjection\Compiler; +namespace Symfony\Component\EventDispatcher\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; @@ -82,12 +82,10 @@ public function process(ContainerBuilder $container) if (is_string($params)) { $priority = 0; $event_subscriber_info[$event_name][$priority][] = array('service' => array($id, $params)); - } - elseif (is_string($params[0])) { + } elseif (is_string($params[0])) { $priority = isset($params[1]) ? $params[1] : 0; $event_subscriber_info[$event_name][$priority][] = array('service' => array($id, $params[0])); - } - else { + } else { foreach ($params as $listener) { $priority = isset($listener[1]) ? $listener[1] : 0; $event_subscriber_info[$event_name][$priority][] = array('service' => array($id, $listener[0])); diff --git a/src/Symfony/Component/EventDispatcher/DrupalEventDispatcher.php b/src/Symfony/Component/EventDispatcher/DrupalEventDispatcher.php index 58efeba24a03a..6b78db9bedb1c 100644 --- a/src/Symfony/Component/EventDispatcher/DrupalEventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/DrupalEventDispatcher.php @@ -37,9 +37,8 @@ * * */ -class ContainerAwareEventDispatcher implements EventDispatcherInterface +class DrupalEventDispatcher implements EventDispatcherInterface { - /** * The service container. * @@ -94,7 +93,7 @@ public function __construct(IntrospectableContainerInterface $container, array $ /** * {@inheritdoc} */ - public function dispatch($event_name, Event $event = NULL) + public function dispatch($event_name, Event $event = null) { if ($event === NULL) { $event = new Event(); @@ -131,7 +130,7 @@ public function dispatch($event_name, Event $event = NULL) /** * {@inheritdoc} */ - public function getListeners($event_name = NULL) + public function getListeners($event_name = null) { $result = array(); @@ -143,8 +142,7 @@ public function getListeners($event_name = NULL) $result[$event_name] = $listeners; } } - } - elseif (isset($this->listeners[$event_name])) { + } elseif (isset($this->listeners[$event_name])) { // Sort listeners if necessary. if (isset($this->unsorted[$event_name])) { krsort($this->listeners[$event_name]); @@ -169,7 +167,7 @@ public function getListeners($event_name = NULL) /** * {@inheritdoc} */ - public function hasListeners($event_name = NULL) + public function hasListeners($event_name = null) { return (bool) count($this->getListeners($event_name)); } @@ -180,7 +178,7 @@ public function hasListeners($event_name = NULL) public function addListener($event_name, $listener, $priority = 0) { $this->listeners[$event_name][$priority][] = array('callable' => $listener); - $this->unsorted[$event_name] = TRUE; + $this->unsorted[$event_name] = true; } /** @@ -216,11 +214,9 @@ public function addSubscriber(EventSubscriberInterface $subscriber) foreach ($subscriber->getSubscribedEvents() as $event_name => $params) { if (is_string($params)) { $this->addListener($event_name, array($subscriber, $params)); - } - elseif (is_string($params[0])) { + } elseif (is_string($params[0])) { $this->addListener($event_name, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0); - } - else { + } else { foreach ($params as $listener) { $this->addListener($event_name, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0); } @@ -238,8 +234,7 @@ public function removeSubscriber(EventSubscriberInterface $subscriber) foreach ($params as $listener) { $this->removeListener($event_name, array($subscriber, $listener[0])); } - } - else { + } else { $this->removeListener($event_name, array($subscriber, is_string($params) ? $params : $params[0])); } } From eb07cb975a0fe576598a25a99a4ab41e1fec8721 Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Thu, 20 Nov 2014 12:42:20 +0100 Subject: [PATCH 03/18] Rename DrupalEventDispatcher to CompiledEventDispatcher --- ...{DrupalEventDispatcher.php => CompiledEventDispatcher.php} | 2 +- ...terListenersPass.php => CompiledRegisterListenersPass.php} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Symfony/Component/EventDispatcher/{DrupalEventDispatcher.php => CompiledEventDispatcher.php} (99%) rename src/Symfony/Component/EventDispatcher/DependencyInjection/{DrupalRegisterListenersPass.php => CompiledRegisterListenersPass.php} (96%) diff --git a/src/Symfony/Component/EventDispatcher/DrupalEventDispatcher.php b/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php similarity index 99% rename from src/Symfony/Component/EventDispatcher/DrupalEventDispatcher.php rename to src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php index 6b78db9bedb1c..d0761c8a45d9e 100644 --- a/src/Symfony/Component/EventDispatcher/DrupalEventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php @@ -37,7 +37,7 @@ * * */ -class DrupalEventDispatcher implements EventDispatcherInterface +class CompiledEventDispatcher implements EventDispatcherInterface { /** * The service container. diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/DrupalRegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php similarity index 96% rename from src/Symfony/Component/EventDispatcher/DependencyInjection/DrupalRegisterListenersPass.php rename to src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php index a664432a94ef7..731f255ff3a8b 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/DrupalRegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php @@ -15,9 +15,9 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; /** - * Compiler pass to register tagged services for an event dispatcher. + * Compiler pass to register tagged services for a compiled event dispatcher. */ -class DrupalRegisterListenersPass implements CompilerPassInterface +class CompiledRegisterListenersPass implements CompilerPassInterface { /** * @var string From 973b0d30fcf25fb173fe797bf88f6f5ba0424276 Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Fri, 21 Nov 2014 08:21:00 +0100 Subject: [PATCH 04/18] Use camel-case for variable names, initialize $unsorted on declaration, remove unused loop index variable --- .../CompiledEventDispatcher.php | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php b/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php index d0761c8a45d9e..c254e03913dbc 100644 --- a/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php @@ -65,9 +65,9 @@ class CompiledEventDispatcher implements EventDispatcherInterface /** * Whether listeners need to be sorted prior to dispatch, keyed by event name. * - * @var TRUE[] + * @var array */ - private $unsorted; + private $unsorted = array(); /** * Constructs a container aware event dispatcher. @@ -87,36 +87,35 @@ public function __construct(IntrospectableContainerInterface $container, array $ { $this->container = $container; $this->listeners = $listeners; - $this->unsorted = array(); } /** * {@inheritdoc} */ - public function dispatch($event_name, Event $event = null) + public function dispatch($eventName, Event $event = null) { if ($event === NULL) { $event = new Event(); } $event->setDispatcher($this); - $event->setName($event_name); + $event->setName($eventName); - if (isset($this->listeners[$event_name])) { + if (isset($this->listeners[$eventName])) { // Sort listeners if necessary. - if (isset($this->unsorted[$event_name])) { - krsort($this->listeners[$event_name]); - unset($this->unsorted[$event_name]); + if (isset($this->unsorted[$eventName])) { + krsort($this->listeners[$eventName]); + unset($this->unsorted[$eventName]); } // Invoke listeners and resolve callables if necessary. - foreach ($this->listeners[$event_name] as $priority => &$definitions) { + foreach ($this->listeners[$eventName] as &$definitions) { foreach ($definitions as $key => &$definition) { if (!isset($definition['callable'])) { $definition['callable'] = array($this->container->get($definition['service'][0]), $definition['service'][1]); } - $definition['callable']($event, $event_name, $this); + $definition['callable']($event, $eventName, $this); if ($event->isPropagationStopped()) { return $event; } @@ -130,27 +129,27 @@ public function dispatch($event_name, Event $event = null) /** * {@inheritdoc} */ - public function getListeners($event_name = null) + public function getListeners($eventName = null) { $result = array(); - if ($event_name === NULL) { + if ($eventName === NULL) { // If event name was omitted, collect all listeners of all events. - foreach (array_keys($this->listeners) as $event_name) { - $listeners = $this->getListeners($event_name); + foreach (array_keys($this->listeners) as $eventName) { + $listeners = $this->getListeners($eventName); if (!empty($listeners)) { - $result[$event_name] = $listeners; + $result[$eventName] = $listeners; } } - } elseif (isset($this->listeners[$event_name])) { + } elseif (isset($this->listeners[$eventName])) { // Sort listeners if necessary. - if (isset($this->unsorted[$event_name])) { - krsort($this->listeners[$event_name]); - unset($this->unsorted[$event_name]); + if (isset($this->unsorted[$eventName])) { + krsort($this->listeners[$eventName]); + unset($this->unsorted[$eventName]); } // Collect listeners and resolve callables if necessary. - foreach ($this->listeners[$event_name] as $priority => &$definitions) { + foreach ($this->listeners[$eventName] as &$definitions) { foreach ($definitions as $key => &$definition) { if (!isset($definition['callable'])) { $definition['callable'] = array($this->container->get($definition['service'][0]), $definition['service'][1]); @@ -167,30 +166,30 @@ public function getListeners($event_name = null) /** * {@inheritdoc} */ - public function hasListeners($event_name = null) + public function hasListeners($eventName = null) { - return (bool) count($this->getListeners($event_name)); + return (bool) count($this->getListeners($eventName)); } /** * {@inheritdoc} */ - public function addListener($event_name, $listener, $priority = 0) + public function addListener($eventName, $listener, $priority = 0) { - $this->listeners[$event_name][$priority][] = array('callable' => $listener); - $this->unsorted[$event_name] = true; + $this->listeners[$eventName][$priority][] = array('callable' => $listener); + $this->unsorted[$eventName] = true; } /** * {@inheritdoc} */ - public function removeListener($event_name, $listener) + public function removeListener($eventName, $listener) { - if (!isset($this->listeners[$event_name])) { + if (!isset($this->listeners[$eventName])) { return; } - foreach ($this->listeners[$event_name] as $priority => $definitions) { + foreach ($this->listeners[$eventName] as $priority => $definitions) { foreach ($definitions as $key => $definition) { if (!isset($definition['callable'])) { if (!$this->container->initialized($definition['service'][0])) { @@ -200,7 +199,7 @@ public function removeListener($event_name, $listener) } if ($definition['callable'] === $listener) { - unset($this->listeners[$event_name][$priority][$key]); + unset($this->listeners[$eventName][$priority][$key]); } } } @@ -211,14 +210,14 @@ public function removeListener($event_name, $listener) */ public function addSubscriber(EventSubscriberInterface $subscriber) { - foreach ($subscriber->getSubscribedEvents() as $event_name => $params) { + foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { if (is_string($params)) { - $this->addListener($event_name, array($subscriber, $params)); + $this->addListener($eventName, array($subscriber, $params)); } elseif (is_string($params[0])) { - $this->addListener($event_name, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0); + $this->addListener($eventName, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0); } else { foreach ($params as $listener) { - $this->addListener($event_name, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0); + $this->addListener($eventName, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0); } } } @@ -229,13 +228,13 @@ public function addSubscriber(EventSubscriberInterface $subscriber) */ public function removeSubscriber(EventSubscriberInterface $subscriber) { - foreach ($subscriber->getSubscribedEvents() as $event_name => $params) { + foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { if (is_array($params) && is_array($params[0])) { foreach ($params as $listener) { - $this->removeListener($event_name, array($subscriber, $listener[0])); + $this->removeListener($eventName, array($subscriber, $listener[0])); } } else { - $this->removeListener($event_name, array($subscriber, is_string($params) ? $params : $params[0])); + $this->removeListener($eventName, array($subscriber, is_string($params) ? $params : $params[0])); } } } From 25482308b0866b9f7525ebbb9870f6d5ac30a8df Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Fri, 21 Nov 2014 08:50:11 +0100 Subject: [PATCH 05/18] Add test covering the EventDispatcherInterface part of CompiledEventDispatcher --- .../Tests/CompiledEventDispatcherTest.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php diff --git a/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php new file mode 100644 index 0000000000000..4abba6d0af4ec --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Tests; + +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\Scope; +use Symfony\Component\EventDispatcher\CompiledEventDispatcher; +use Symfony\Component\EventDispatcher\Event; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +class CompiledEventDispatcherTest extends AbstractEventDispatcherTest +{ + protected function createEventDispatcher() + { + $container = new Container(); + return new CompiledEventDispatcher($container); + } +} From 56d8ce5cba9caef140a569358d5e1b55467418f5 Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Fri, 21 Nov 2014 08:57:13 +0100 Subject: [PATCH 06/18] Add support for tagged listeners to CompiledRegisterListenersPass --- .../CompiledRegisterListenersPass.php | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php index 731f255ff3a8b..ced465d48a296 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php @@ -59,15 +59,43 @@ public function process(ContainerBuilder $container) $definition = $container->findDefinition($this->dispatcherService); - // TODO: Also collect services tagged as listeners. - $event_subscriber_info = array(); - foreach ($container->findTaggedServiceIds($this->subscriberTag) as $id => $attributes) { + + foreach ($container->findTaggedServiceIds($this->listenerTag) as $id => $events) { $def = $container->getDefinition($id); if (!$def->isPublic()) { throw new \InvalidArgumentException(sprintf('The service "%s" must be public as event listeners are lazy-loaded.', $id)); } + if ($def->isAbstract()) { + throw new \InvalidArgumentException(sprintf('The service "%s" must not be abstract as event listeners are lazy-loaded.', $id)); + } + + foreach ($events as $event) { + $priority = isset($event['priority']) ? $event['priority'] : 0; + + if (!isset($event['event'])) { + throw new \InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag)); + } + + if (!isset($event['method'])) { + $event['method'] = 'on'.preg_replace_callback(array( + '/(?<=\b)[a-z]/i', + '/[^a-z0-9]/i', + ), function ($matches) { return strtoupper($matches[0]); }, $event['event']); + $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']); + } + + $event_subscriber_info[$event['event']][$priority][] = array('service' => array($id, $event['method'])); + } + } + + foreach ($container->findTaggedServiceIds($this->subscriberTag) as $id => $attributes) { + $def = $container->getDefinition($id); + if (!$def->isPublic()) { + throw new \InvalidArgumentException(sprintf('The service "%s" must be public as event subscribers are lazy-loaded.', $id)); + } + // We must assume that the class value has been correctly filled, even if the service is created by a factory $class = $def->getClass(); From 002cf8bbe090982b8a6eae0c1d92886bce2d0bad Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Fri, 21 Nov 2014 09:01:21 +0100 Subject: [PATCH 07/18] Use camel-case for variable names --- .../CompiledRegisterListenersPass.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php index ced465d48a296..8cc4d3e93da7f 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php @@ -59,7 +59,7 @@ public function process(ContainerBuilder $container) $definition = $container->findDefinition($this->dispatcherService); - $event_subscriber_info = array(); + $listeners = array(); foreach ($container->findTaggedServiceIds($this->listenerTag) as $id => $events) { $def = $container->getDefinition($id); @@ -86,7 +86,7 @@ public function process(ContainerBuilder $container) $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']); } - $event_subscriber_info[$event['event']][$priority][] = array('service' => array($id, $event['method'])); + $listeners[$event['event']][$priority][] = array('service' => array($id, $event['method'])); } } @@ -106,26 +106,26 @@ public function process(ContainerBuilder $container) } // Get all subscribed events. - foreach ($class::getSubscribedEvents() as $event_name => $params) { + foreach ($class::getSubscribedEvents() as $eventName => $params) { if (is_string($params)) { $priority = 0; - $event_subscriber_info[$event_name][$priority][] = array('service' => array($id, $params)); + $listeners[$eventName][$priority][] = array('service' => array($id, $params)); } elseif (is_string($params[0])) { $priority = isset($params[1]) ? $params[1] : 0; - $event_subscriber_info[$event_name][$priority][] = array('service' => array($id, $params[0])); + $listeners[$eventName][$priority][] = array('service' => array($id, $params[0])); } else { foreach ($params as $listener) { $priority = isset($listener[1]) ? $listener[1] : 0; - $event_subscriber_info[$event_name][$priority][] = array('service' => array($id, $listener[0])); + $listeners[$eventName][$priority][] = array('service' => array($id, $listener[0])); } } } } - foreach (array_keys($event_subscriber_info) as $event_name) { - krsort($event_subscriber_info[$event_name]); + foreach (array_keys($listeners) as $eventName) { + krsort($listeners[$eventName]); } - $definition->addArgument($event_subscriber_info); + $definition->addArgument($listeners); } } From 2e39ed6330368a733d80349c087b896882f9d8e3 Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Fri, 21 Nov 2014 10:25:32 +0100 Subject: [PATCH 08/18] Add test coverage for CompiledRegisterListenersPass --- .../CompiledRegisterListenersPass.php | 2 +- .../CompiledRegisterListenersPassTest.php | 158 ++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php index 8cc4d3e93da7f..379dabf7b55da 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php @@ -102,7 +102,7 @@ public function process(ContainerBuilder $container) $refClass = new \ReflectionClass($class); $interface = 'Symfony\Component\EventDispatcher\EventSubscriberInterface'; if (!$refClass->implementsInterface($interface)) { - throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface)); + throw new \InvalidArgumentException(sprintf('The service "%s" must implement interface "%s".', $id, $interface)); } // Get all subscribed events. diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php new file mode 100644 index 0000000000000..db0277ae1932c --- /dev/null +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Tests\DependencyInjection; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\EventDispatcher\DependencyInjection\CompiledRegisterListenersPass; + +class CompiledCompiledRegisterListenersPassTest extends \PHPUnit_Framework_TestCase +{ + public function testPassAddsConstructorArgument() { + $container = new ContainerBuilder(); + $definition = $container->register('event_dispatcher', 'stdClass') + ->setArguments(array('foo', 'bar')); + + $registerListenersPass = new CompiledRegisterListenersPass(); + $registerListenersPass->process($container); + + $expected_arguments = array('foo', 'bar', array()); + $this->assertSame($expected_arguments, $definition->getArguments()); + } + + public function testPassAddsTaggedListenersAndSubscribers() { + $container = new ContainerBuilder(); + $definition = $container->register('event_dispatcher', 'stdClass'); + + $container->register('test_subscriber', 'Symfony\Component\EventDispatcher\Tests\DependencyInjection\SubscriberService') + ->addTag('kernel.event_subscriber'); + + $container->register('test_listener', 'stdObject') + ->addTag('kernel.event_listener', array( + 'event' => 'test_event.multiple_listeners', + 'method' => 'methodWithMediumPriority', + 'priority' => 32, + )); + + $registerListenersPass = new CompiledRegisterListenersPass(); + $registerListenersPass->process($container); + + $expected_listeners = array( + 'test_event.multiple_listeners' => array( + 128 => array( + array( + 'service' => array('test_subscriber', 'methodWithHighestPriority'), + ), + ), + 32 => array( + array( + 'service' => array('test_listener', 'methodWithMediumPriority'), + ), + ), + 0 => array( + array( + 'service' => array('test_subscriber', 'methodWithoutPriority'), + ), + ), + ), + 'test_event.single_listener_with_priority' => array( + 64 => array( + array( + 'service' => array('test_subscriber', 'methodWithHighPriority'), + ) + ), + ), + 'test_event.single_listener_without_priority' => array( + 0 => array( + array( + 'service' => array('test_subscriber', 'methodWithoutPriority'), + ), + ), + ), + ); + $this->assertSame(array($expected_listeners), $definition->getArguments()); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The service "foo" must implement interface "Symfony\Component\EventDispatcher\EventSubscriberInterface". + */ + public function testEventSubscriberWithoutInterface() + { + $container = new ContainerBuilder(); + $container->register('foo', 'stdClass')->addTag('kernel.event_subscriber', array()); + $container->register('event_dispatcher', 'stdClass'); + + $registerListenersPass = new CompiledRegisterListenersPass(); + $registerListenersPass->process($container); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The service "foo" must be public as event listeners are lazy-loaded. + */ + public function testPrivateEventListener() + { + $container = new ContainerBuilder(); + $container->register('foo', 'stdClass')->setPublic(false)->addTag('kernel.event_listener', array()); + $container->register('event_dispatcher', 'stdClass'); + + $registerListenersPass = new CompiledRegisterListenersPass(); + $registerListenersPass->process($container); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The service "foo" must be public as event subscribers are lazy-loaded. + */ + public function testPrivateEventSubscriber() + { + $container = new ContainerBuilder(); + $container->register('foo', 'stdClass')->setPublic(false)->addTag('kernel.event_subscriber', array()); + $container->register('event_dispatcher', 'stdClass'); + + $registerListenersPass = new CompiledRegisterListenersPass(); + $registerListenersPass->process($container); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The service "foo" must not be abstract as event listeners are lazy-loaded. + */ + public function testAbstractEventListener() + { + $container = new ContainerBuilder(); + $container->register('foo', 'stdClass')->setAbstract(true)->addTag('kernel.event_listener', array()); + $container->register('event_dispatcher', 'stdClass'); + + $registerListenersPass = new CompiledRegisterListenersPass(); + $registerListenersPass->process($container); + } +} + +class SubscriberService implements \Symfony\Component\EventDispatcher\EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return array( + 'test_event.multiple_listeners' => array( + array('methodWithHighestPriority', 128), + array('methodWithoutPriority'), + ), + 'test_event.single_listener_with_priority' => array( + array('methodWithHighPriority', 64), + ), + 'test_event.single_listener_without_priority' => array( + array('methodWithoutPriority'), + ), + ); + } +} From a2cd25024b9d321d12bfe122e991b90bf6351ecf Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Fri, 21 Nov 2014 11:37:55 +0100 Subject: [PATCH 09/18] Complete test coverage for CompiledEventDispatcher --- .../Tests/CompiledEventDispatcherTest.php | 139 +++++++++++++++++- 1 file changed, 136 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php index 4abba6d0af4ec..5f4233bc09666 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php @@ -12,10 +12,8 @@ namespace Symfony\Component\EventDispatcher\Tests; use Symfony\Component\DependencyInjection\Container; -use Symfony\Component\DependencyInjection\Scope; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\CompiledEventDispatcher; -use Symfony\Component\EventDispatcher\Event; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; class CompiledEventDispatcherTest extends AbstractEventDispatcherTest { @@ -24,4 +22,139 @@ protected function createEventDispatcher() $container = new Container(); return new CompiledEventDispatcher($container); } + + public function testGetListenersWithCallables() { + // When passing in callables exclusively as listeners into the event + // dispatcher constructor, the event dispatcher must not attempt to + // resolve any services. + $container = $this->getMock('Symfony\Component\DependencyInjection\IntrospectableContainerInterface'); + $container->expects($this->never())->method($this->anything()); + + $firstListener = new CallableClass(); + $secondListener = function () {}; + $thirdListener = array(new TestEventListener(), 'preFoo'); + $listeners = array( + 'test_event' => array( + 0 => array( + array('callable' => $firstListener), + array('callable' => $secondListener), + array('callable' => $thirdListener), + ), + ), + ); + + $dispatcher = new CompiledEventDispatcher($container, $listeners); + $actualListeners = $dispatcher->getListeners(); + + $expectedListeners = array( + 'test_event' => array( + $firstListener, + $secondListener, + $thirdListener, + ), + ); + + $this->assertSame($expectedListeners, $actualListeners); + } + + public function testDispatchWithCallables() { + // When passing in callables exclusively as listeners into the event + // dispatcher constructor, the event dispatcher must not attempt to + // resolve any services. + $container = $this->getMock('Symfony\Component\DependencyInjection\IntrospectableContainerInterface'); + $container->expects($this->never())->method($this->anything()); + + $firstListener = new CallableClass(); + $secondListener = function () {}; + $thirdListener = array(new TestEventListener(), 'preFoo'); + $listeners = array( + 'test_event' => array( + 0 => array( + array('callable' => $firstListener), + array('callable' => $secondListener), + array('callable' => $thirdListener), + ), + ), + ); + + $dispatcher = new CompiledEventDispatcher($container, $listeners); + $dispatcher->dispatch('test_event'); + + $this->assertTrue($thirdListener[0]->preFooInvoked); + } + + public function testGetListenersWithServices() { + $container = new ContainerBuilder(); + $container->register('listener_service', 'Symfony\Component\EventDispatcher\Tests\TestEventListener'); + + $listeners = array( + 'test_event' => array( + 0 => array( + array('service' => array('listener_service', 'preFoo')), + ), + ), + ); + + $dispatcher = new CompiledEventDispatcher($container, $listeners); + $actualListeners = $dispatcher->getListeners(); + + $listenerService = $container->get('listener_service'); + $expectedListeners = array( + 'test_event' => array( + array($listenerService, 'preFoo'), + ), + ); + + $this->assertSame($expectedListeners, $actualListeners); + } + + public function testDispatchWithServices() { + $container = new ContainerBuilder(); + $container->register('listener_service', 'Symfony\Component\EventDispatcher\Tests\TestEventListener'); + + $listeners = array( + 'test_event' => array( + 0 => array( + array('service' => array('listener_service', 'preFoo')), + ), + ), + ); + + $dispatcher = new CompiledEventDispatcher($container, $listeners); + + $dispatcher->dispatch('test_event'); + + $listenerService = $container->get('listener_service'); + $this->assertTrue($listenerService->preFooInvoked); + } + + public function testRemoveService() { + $container = new ContainerBuilder(); + $container->register('listener_service', 'Symfony\Component\EventDispatcher\Tests\TestEventListener'); + $container->register('other_listener_service', 'Symfony\Component\EventDispatcher\Tests\TestEventListener'); + + $listeners = array( + 'test_event' => array( + 0 => array( + array('service' => array('listener_service', 'preFoo')), + array('service' => array('other_listener_service', 'preFoo')), + ), + ), + ); + + $dispatcher = new CompiledEventDispatcher($container, $listeners); + + $listenerService = $container->get('listener_service'); + $dispatcher->removeListener('test_event', array($listenerService, 'preFoo')); + + // Ensure that other service was not initialized during removal of the + // listener service. + $this->assertFalse($container->initialized('other_listener_service')); + + $dispatcher->dispatch('test_event'); + + $this->assertFalse($listenerService->preFooInvoked); + $otherService = $container->get('other_listener_service'); + $this->assertTrue($otherService->preFooInvoked); + } } From 60845da0c60db3fec7e3c2d4b7407990b72855b9 Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Fri, 21 Nov 2014 17:10:46 +0100 Subject: [PATCH 10/18] Fix colliding mock subscriber classes --- .../CompiledRegisterListenersPassTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php index db0277ae1932c..01576e0b57d4b 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php @@ -14,7 +14,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\DependencyInjection\CompiledRegisterListenersPass; -class CompiledCompiledRegisterListenersPassTest extends \PHPUnit_Framework_TestCase +class CompiledRegisterListenersPassTest extends \PHPUnit_Framework_TestCase { public function testPassAddsConstructorArgument() { $container = new ContainerBuilder(); @@ -32,7 +32,7 @@ public function testPassAddsTaggedListenersAndSubscribers() { $container = new ContainerBuilder(); $definition = $container->register('event_dispatcher', 'stdClass'); - $container->register('test_subscriber', 'Symfony\Component\EventDispatcher\Tests\DependencyInjection\SubscriberService') + $container->register('test_subscriber', 'Symfony\Component\EventDispatcher\Tests\DependencyInjection\CompiledSubscriberService') ->addTag('kernel.event_subscriber'); $container->register('test_listener', 'stdObject') @@ -138,7 +138,7 @@ public function testAbstractEventListener() } } -class SubscriberService implements \Symfony\Component\EventDispatcher\EventSubscriberInterface +class CompiledSubscriberService implements \Symfony\Component\EventDispatcher\EventSubscriberInterface { public static function getSubscribedEvents() { From 3a4f177f9e4f11d8327bca3322a363a3317d32dc Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Fri, 21 Nov 2014 17:13:25 +0100 Subject: [PATCH 11/18] PHP 5.3 does not support callables in array($object, $method) form --- .../Component/EventDispatcher/CompiledEventDispatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php b/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php index c254e03913dbc..020b0a561a8ab 100644 --- a/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php @@ -115,7 +115,7 @@ public function dispatch($eventName, Event $event = null) $definition['callable'] = array($this->container->get($definition['service'][0]), $definition['service'][1]); } - $definition['callable']($event, $eventName, $this); + call_user_func($definition['callable'], $event, $eventName, $this); if ($event->isPropagationStopped()) { return $event; } From 273a397fe4bb2165bf230c04f334361c56ee4bce Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Fri, 21 Nov 2014 17:15:38 +0100 Subject: [PATCH 12/18] Fix coding style --- .../Tests/CompiledEventDispatcherTest.php | 16 +++++++++++----- .../CompiledRegisterListenersPassTest.php | 8 +++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php index 5f4233bc09666..dc9fd52a0bc17 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php @@ -20,10 +20,12 @@ class CompiledEventDispatcherTest extends AbstractEventDispatcherTest protected function createEventDispatcher() { $container = new Container(); + return new CompiledEventDispatcher($container); } - public function testGetListenersWithCallables() { + public function testGetListenersWithCallables() + { // When passing in callables exclusively as listeners into the event // dispatcher constructor, the event dispatcher must not attempt to // resolve any services. @@ -57,7 +59,8 @@ public function testGetListenersWithCallables() { $this->assertSame($expectedListeners, $actualListeners); } - public function testDispatchWithCallables() { + public function testDispatchWithCallables() + { // When passing in callables exclusively as listeners into the event // dispatcher constructor, the event dispatcher must not attempt to // resolve any services. @@ -83,7 +86,8 @@ public function testDispatchWithCallables() { $this->assertTrue($thirdListener[0]->preFooInvoked); } - public function testGetListenersWithServices() { + public function testGetListenersWithServices() + { $container = new ContainerBuilder(); $container->register('listener_service', 'Symfony\Component\EventDispatcher\Tests\TestEventListener'); @@ -108,7 +112,8 @@ public function testGetListenersWithServices() { $this->assertSame($expectedListeners, $actualListeners); } - public function testDispatchWithServices() { + public function testDispatchWithServices() + { $container = new ContainerBuilder(); $container->register('listener_service', 'Symfony\Component\EventDispatcher\Tests\TestEventListener'); @@ -128,7 +133,8 @@ public function testDispatchWithServices() { $this->assertTrue($listenerService->preFooInvoked); } - public function testRemoveService() { + public function testRemoveService() + { $container = new ContainerBuilder(); $container->register('listener_service', 'Symfony\Component\EventDispatcher\Tests\TestEventListener'); $container->register('other_listener_service', 'Symfony\Component\EventDispatcher\Tests\TestEventListener'); diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php index 01576e0b57d4b..96be106d9e990 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php @@ -16,7 +16,8 @@ class CompiledRegisterListenersPassTest extends \PHPUnit_Framework_TestCase { - public function testPassAddsConstructorArgument() { + public function testPassAddsConstructorArgument() + { $container = new ContainerBuilder(); $definition = $container->register('event_dispatcher', 'stdClass') ->setArguments(array('foo', 'bar')); @@ -28,7 +29,8 @@ public function testPassAddsConstructorArgument() { $this->assertSame($expected_arguments, $definition->getArguments()); } - public function testPassAddsTaggedListenersAndSubscribers() { + public function testPassAddsTaggedListenersAndSubscribers() + { $container = new ContainerBuilder(); $definition = $container->register('event_dispatcher', 'stdClass'); @@ -67,7 +69,7 @@ public function testPassAddsTaggedListenersAndSubscribers() { 64 => array( array( 'service' => array('test_subscriber', 'methodWithHighPriority'), - ) + ), ), ), 'test_event.single_listener_without_priority' => array( From 02d84f7e93c7c3160842c03c811c52c784ee09be Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Sat, 6 Dec 2014 16:52:44 +0100 Subject: [PATCH 13/18] Fix docs on compiled event dispatcher --- .../Component/EventDispatcher/CompiledEventDispatcher.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php b/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php index 020b0a561a8ab..54c69a49eb913 100644 --- a/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php @@ -24,8 +24,8 @@ *
* Instead of calling addSubscriberService once for each * subscriber, a precompiled array of listener definitions is passed - * directly to the constructor. This is faster by roughly an order of - * magnitude. The listeners are collected and prepared using a compiler + * directly to the constructor. This is faster by an order of magnitude. + * The listeners are collected and prepared using a compiler * pass. *
*
Lazy instantiation of listeners
From 0150640f79bb2f0a525174b888002fa22fa5e366 Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Mon, 8 Dec 2014 10:39:08 +0100 Subject: [PATCH 14/18] Use private visibility for instance variables --- .../CompiledRegisterListenersPass.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php index 379dabf7b55da..7cedc307829f3 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php @@ -20,25 +20,31 @@ class CompiledRegisterListenersPass implements CompilerPassInterface { /** + * Service name of the event dispatcher in processed container. + * * @var string */ - protected $dispatcherService; + private $dispatcherService; /** + * Tag name used for listeners. + * * @var string */ - protected $listenerTag; + private $listenerTag; /** + * Tag name used for subscribers. + * * @var string */ - protected $subscriberTag; + private $subscriberTag; /** * Constructor. * * @param string $dispatcherService Service name of the event dispatcher in processed container - * @param string $listenerTag Tag name used for listener + * @param string $listenerTag Tag name used for listeners * @param string $subscriberTag Tag name used for subscribers */ public function __construct($dispatcherService = 'event_dispatcher', $listenerTag = 'kernel.event_listener', $subscriberTag = 'kernel.event_subscriber') From d0f986df87bf77550cb0cd47696e2ab1e61bc1c2 Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Mon, 5 Jan 2015 14:43:53 +0100 Subject: [PATCH 15/18] Fix codestyle as suggested by @stof --- .../CompiledEventDispatcher.php | 42 +++++++++++-------- .../CompiledRegisterListenersPassTest.php | 3 +- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php b/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php index 54c69a49eb913..21b2ac2bb3f5c 100644 --- a/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php @@ -42,7 +42,7 @@ class CompiledEventDispatcher implements EventDispatcherInterface /** * The service container. * - * @var \Symfony\Component\DependencyInjection\IntrospectableContainerInterface + * @var IntrospectableContainerInterface */ private $container; @@ -72,7 +72,7 @@ class CompiledEventDispatcher implements EventDispatcherInterface /** * Constructs a container aware event dispatcher. * - * @param \Symfony\Component\EventDispatcher\IntrospectableContainerInterface $container + * @param IntrospectableContainerInterface $container * The service container. * @param array $listeners * A nested array of listener definitions keyed by event name and priority. @@ -94,7 +94,7 @@ public function __construct(IntrospectableContainerInterface $container, array $ */ public function dispatch($eventName, Event $event = null) { - if ($event === NULL) { + if (null === $event) { $event = new Event(); } @@ -110,7 +110,7 @@ public function dispatch($eventName, Event $event = null) // Invoke listeners and resolve callables if necessary. foreach ($this->listeners[$eventName] as &$definitions) { - foreach ($definitions as $key => &$definition) { + foreach ($definitions as &$definition) { if (!isset($definition['callable'])) { $definition['callable'] = array($this->container->get($definition['service'][0]), $definition['service'][1]); } @@ -133,7 +133,7 @@ public function getListeners($eventName = null) { $result = array(); - if ($eventName === NULL) { + if (null === $eventName) { // If event name was omitted, collect all listeners of all events. foreach (array_keys($this->listeners) as $eventName) { $listeners = $this->getListeners($eventName); @@ -141,22 +141,28 @@ public function getListeners($eventName = null) $result[$eventName] = $listeners; } } - } elseif (isset($this->listeners[$eventName])) { - // Sort listeners if necessary. - if (isset($this->unsorted[$eventName])) { - krsort($this->listeners[$eventName]); - unset($this->unsorted[$eventName]); - } - // Collect listeners and resolve callables if necessary. - foreach ($this->listeners[$eventName] as &$definitions) { - foreach ($definitions as $key => &$definition) { - if (!isset($definition['callable'])) { - $definition['callable'] = array($this->container->get($definition['service'][0]), $definition['service'][1]); - } + return $result; + } - $result[] = $definition['callable']; + if (!isset($this->listeners[$eventName])) { + return $result; + } + + // Sort listeners if necessary. + if (isset($this->unsorted[$eventName])) { + krsort($this->listeners[$eventName]); + unset($this->unsorted[$eventName]); + } + + // Collect listeners and resolve callables if necessary. + foreach ($this->listeners[$eventName] as &$definitions) { + foreach ($definitions as &$definition) { + if (!isset($definition['callable'])) { + $definition['callable'] = array($this->container->get($definition['service'][0]), $definition['service'][1]); } + + $result[] = $definition['callable']; } } diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php index 96be106d9e990..d550101cb9c7f 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php @@ -13,6 +13,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\DependencyInjection\CompiledRegisterListenersPass; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; class CompiledRegisterListenersPassTest extends \PHPUnit_Framework_TestCase { @@ -140,7 +141,7 @@ public function testAbstractEventListener() } } -class CompiledSubscriberService implements \Symfony\Component\EventDispatcher\EventSubscriberInterface +class CompiledSubscriberService implements EventSubscriberInterface { public static function getSubscribedEvents() { From 0bee27f2faef077616720244482d914e6af7b7df Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Mon, 5 Jan 2015 15:07:52 +0100 Subject: [PATCH 16/18] Use string keys for service definition --- .../EventDispatcher/CompiledEventDispatcher.php | 14 ++++++++------ .../CompiledRegisterListenersPass.php | 8 ++++---- .../Tests/CompiledEventDispatcherTest.php | 8 ++++---- .../CompiledRegisterListenersPassTest.php | 10 +++++----- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php b/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php index 21b2ac2bb3f5c..63407768d39cf 100644 --- a/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php +++ b/src/Symfony/Component/EventDispatcher/CompiledEventDispatcher.php @@ -53,7 +53,8 @@ class CompiledEventDispatcher implements EventDispatcherInterface * A listener definition is an associative array with one of the following key * value pairs: * - callable: A callable listener - * - service: An array of the form array(service id, method) + * - service: An array of the form + * array('id' => service id, 'method' => method name) * * A service entry will be resolved to a callable only just before its * invocation. @@ -79,7 +80,8 @@ class CompiledEventDispatcher implements EventDispatcherInterface * The array is expected to be ordered by priority. A listener definition is * an associative array with one of the following key value pairs: * - callable: A callable listener - * - service: An array of the form array(service id, method) + * - service: An array of the form + * array('id' => service id, 'method' => method name) * A service entry will be resolved to a callable only just before its * invocation. */ @@ -112,7 +114,7 @@ public function dispatch($eventName, Event $event = null) foreach ($this->listeners[$eventName] as &$definitions) { foreach ($definitions as &$definition) { if (!isset($definition['callable'])) { - $definition['callable'] = array($this->container->get($definition['service'][0]), $definition['service'][1]); + $definition['callable'] = array($this->container->get($definition['service']['id']), $definition['service']['method']); } call_user_func($definition['callable'], $event, $eventName, $this); @@ -159,7 +161,7 @@ public function getListeners($eventName = null) foreach ($this->listeners[$eventName] as &$definitions) { foreach ($definitions as &$definition) { if (!isset($definition['callable'])) { - $definition['callable'] = array($this->container->get($definition['service'][0]), $definition['service'][1]); + $definition['callable'] = array($this->container->get($definition['service']['id']), $definition['service']['method']); } $result[] = $definition['callable']; @@ -198,10 +200,10 @@ public function removeListener($eventName, $listener) foreach ($this->listeners[$eventName] as $priority => $definitions) { foreach ($definitions as $key => $definition) { if (!isset($definition['callable'])) { - if (!$this->container->initialized($definition['service'][0])) { + if (!$this->container->initialized($definition['service']['id'])) { continue; } - $definition['callable'] = array($this->container->get($definition['service'][0]), $definition['service'][1]); + $definition['callable'] = array($this->container->get($definition['service']['id']), $definition['service']['method']); } if ($definition['callable'] === $listener) { diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php index 7cedc307829f3..8897774520c48 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php @@ -92,7 +92,7 @@ public function process(ContainerBuilder $container) $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']); } - $listeners[$event['event']][$priority][] = array('service' => array($id, $event['method'])); + $listeners[$event['event']][$priority][] = array('service' => array('id' => $id, 'method' => $event['method'])); } } @@ -115,14 +115,14 @@ public function process(ContainerBuilder $container) foreach ($class::getSubscribedEvents() as $eventName => $params) { if (is_string($params)) { $priority = 0; - $listeners[$eventName][$priority][] = array('service' => array($id, $params)); + $listeners[$eventName][$priority][] = array('service' => array('id' => $id, 'method' => $params)); } elseif (is_string($params[0])) { $priority = isset($params[1]) ? $params[1] : 0; - $listeners[$eventName][$priority][] = array('service' => array($id, $params[0])); + $listeners[$eventName][$priority][] = array('service' => array('id' => $id, 'method' => $params[0])); } else { foreach ($params as $listener) { $priority = isset($listener[1]) ? $listener[1] : 0; - $listeners[$eventName][$priority][] = array('service' => array($id, $listener[0])); + $listeners[$eventName][$priority][] = array('service' => array('id' => $id, 'method' => $listener[0])); } } } diff --git a/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php b/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php index dc9fd52a0bc17..c5c89d2279a0d 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/CompiledEventDispatcherTest.php @@ -94,7 +94,7 @@ public function testGetListenersWithServices() $listeners = array( 'test_event' => array( 0 => array( - array('service' => array('listener_service', 'preFoo')), + array('service' => array('id' => 'listener_service', 'method' => 'preFoo')), ), ), ); @@ -120,7 +120,7 @@ public function testDispatchWithServices() $listeners = array( 'test_event' => array( 0 => array( - array('service' => array('listener_service', 'preFoo')), + array('service' => array('id' => 'listener_service', 'method' => 'preFoo')), ), ), ); @@ -142,8 +142,8 @@ public function testRemoveService() $listeners = array( 'test_event' => array( 0 => array( - array('service' => array('listener_service', 'preFoo')), - array('service' => array('other_listener_service', 'preFoo')), + array('service' => array('id' => 'listener_service', 'method' => 'preFoo')), + array('service' => array('id' => 'other_listener_service', 'method' => 'preFoo')), ), ), ); diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php index d550101cb9c7f..16e3c906d27b5 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php @@ -52,31 +52,31 @@ public function testPassAddsTaggedListenersAndSubscribers() 'test_event.multiple_listeners' => array( 128 => array( array( - 'service' => array('test_subscriber', 'methodWithHighestPriority'), + 'service' => array('id' => 'test_subscriber', 'method' => 'methodWithHighestPriority'), ), ), 32 => array( array( - 'service' => array('test_listener', 'methodWithMediumPriority'), + 'service' => array('id' => 'test_listener', 'method' => 'methodWithMediumPriority'), ), ), 0 => array( array( - 'service' => array('test_subscriber', 'methodWithoutPriority'), + 'service' => array('id' => 'test_subscriber', 'method' => 'methodWithoutPriority'), ), ), ), 'test_event.single_listener_with_priority' => array( 64 => array( array( - 'service' => array('test_subscriber', 'methodWithHighPriority'), + 'service' => array('id' => 'test_subscriber', 'method' => 'methodWithHighPriority'), ), ), ), 'test_event.single_listener_without_priority' => array( 0 => array( array( - 'service' => array('test_subscriber', 'methodWithoutPriority'), + 'service' => array('id' => 'test_subscriber', 'method' => 'methodWithoutPriority'), ), ), ), From 9a9b31cbd3398fe33231ff9d54bc4f2fa39c611b Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Sun, 18 Jan 2015 11:10:33 +0100 Subject: [PATCH 17/18] Replicate #13293 --- .../CompiledRegisterListenersPass.php | 6 +- .../CompiledRegisterListenersPassTest.php | 85 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php index 8897774520c48..81dbd0c80982a 100644 --- a/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php +++ b/src/Symfony/Component/EventDispatcher/DependencyInjection/CompiledRegisterListenersPass.php @@ -102,8 +102,12 @@ public function process(ContainerBuilder $container) throw new \InvalidArgumentException(sprintf('The service "%s" must be public as event subscribers are lazy-loaded.', $id)); } + if ($def->isAbstract()) { + throw new \InvalidArgumentException(sprintf('The service "%s" must not be abstract as event subscribers are lazy-loaded.', $id)); + } + // We must assume that the class value has been correctly filled, even if the service is created by a factory - $class = $def->getClass(); + $class = $container->getParameterBag()->resolveValue($def->getClass()); $refClass = new \ReflectionClass($class); $interface = 'Symfony\Component\EventDispatcher\EventSubscriberInterface'; diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php index 16e3c906d27b5..ba1a273376993 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php @@ -139,6 +139,91 @@ public function testAbstractEventListener() $registerListenersPass = new CompiledRegisterListenersPass(); $registerListenersPass->process($container); } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage The service "foo" must not be abstract as event subscribers are lazy-loaded. + */ + public function testAbstractEventSubscriber() + { + $container = new ContainerBuilder(); + $container->register('foo', 'stdClass')->setAbstract(true)->addTag('kernel.event_subscriber', array()); + $container->register('event_dispatcher', 'stdClass'); + + $registerListenersPass = new CompiledRegisterListenersPass(); + $registerListenersPass->process($container); + } + + public function testEventSubscriberResolvableClassName() + { + $container = new ContainerBuilder(); + + $container->setParameter('subscriber.class', 'Symfony\Component\EventDispatcher\Tests\DependencyInjection\CompiledSubscriberService'); + $container->register('foo', '%subscriber.class%')->addTag('kernel.event_subscriber', array()); + $container->register('event_dispatcher', 'stdClass'); + + $registerListenersPass = new CompiledRegisterListenersPass(); + $registerListenersPass->process($container); + + $definition = $container->getDefinition('event_dispatcher'); + $expected_arguments = array( + array ( + 'test_event.multiple_listeners' => array ( + 128 => array ( + array ( + 'service' => array ( + 'id' => 'foo', + 'method' => 'methodWithHighestPriority', + ), + ), + ), + 0 => array ( + array ( + 'service' => array ( + 'id' => 'foo', + 'method' => 'methodWithoutPriority', + ), + ), + ), + ), + 'test_event.single_listener_with_priority' => array ( + 64 => array ( + array ( + 'service' => array ( + 'id' => 'foo', + 'method' => 'methodWithHighPriority', + ), + ), + ), + ), + 'test_event.single_listener_without_priority' => array ( + 0 => array ( + array ( + 'service' => array ( + 'id' => 'foo', + 'method' => 'methodWithoutPriority', + ), + ), + ), + ), + ), + ); + $this->assertSame($expected_arguments, $definition->getArguments()); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage You have requested a non-existent parameter "subscriber.class" + */ + public function testEventSubscriberUnresolvableClassName() + { + $container = new ContainerBuilder(); + $container->register('foo', '%subscriber.class%')->addTag('kernel.event_subscriber', array()); + $container->register('event_dispatcher', 'stdClass'); + + $registerListenersPass = new CompiledRegisterListenersPass(); + $registerListenersPass->process($container); + } } class CompiledSubscriberService implements EventSubscriberInterface From b4f56ab4bd64b583538dd4f0197157a2b2e863b0 Mon Sep 17 00:00:00 2001 From: Lorenz Schori Date: Sun, 18 Jan 2015 11:18:33 +0100 Subject: [PATCH 18/18] Fix code style --- .../CompiledRegisterListenersPassTest.php | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php index ba1a273376993..d46ffb74b6b97 100644 --- a/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php +++ b/src/Symfony/Component/EventDispatcher/Tests/DependencyInjection/CompiledRegisterListenersPassTest.php @@ -167,39 +167,39 @@ public function testEventSubscriberResolvableClassName() $definition = $container->getDefinition('event_dispatcher'); $expected_arguments = array( - array ( - 'test_event.multiple_listeners' => array ( - 128 => array ( - array ( - 'service' => array ( + array( + 'test_event.multiple_listeners' => array( + 128 => array( + array( + 'service' => array( 'id' => 'foo', 'method' => 'methodWithHighestPriority', ), ), ), - 0 => array ( - array ( - 'service' => array ( + 0 => array( + array( + 'service' => array( 'id' => 'foo', 'method' => 'methodWithoutPriority', ), ), ), ), - 'test_event.single_listener_with_priority' => array ( - 64 => array ( - array ( - 'service' => array ( + 'test_event.single_listener_with_priority' => array( + 64 => array( + array( + 'service' => array( 'id' => 'foo', 'method' => 'methodWithHighPriority', ), ), ), ), - 'test_event.single_listener_without_priority' => array ( - 0 => array ( - array ( - 'service' => array ( + 'test_event.single_listener_without_priority' => array( + 0 => array( + array( + 'service' => array( 'id' => 'foo', 'method' => 'methodWithoutPriority', ),