diff --git a/core/core.services.yml b/core/core.services.yml
index 5583040..dfa9fef 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -380,7 +380,7 @@ services:
class: Drupal\Core\Routing\CurrentRouteMatch
arguments: ['@request_stack']
event_dispatcher:
- class: Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher
+ class: Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher
arguments: ['@service_container']
controller_resolver:
class: Drupal\Core\Controller\ControllerResolver
diff --git a/core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php b/core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php
new file mode 100644
index 0000000..caead14
--- /dev/null
+++ b/core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php
@@ -0,0 +1,238 @@
+
+ *
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;
+ */
+ protected $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 [service id, method]
+ *
+ * A service entry will be resolved to a callable only just before its
+ * invocation.
+ *
+ * @var array
+ */
+ protected $listeners;
+
+ /**
+ * Whether event listeners are ordered, keyed by event name.
+ *
+ * @var TRUE[]
+ */
+ protected $sorted;
+
+ /**
+ * 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 [service id, method]
+ * A service entry will be resolved to a callable only just before its
+ * invocation.
+ */
+ public function __construct(IntrospectableContainerInterface $container, array $listeners = []) {
+ $this->container = $container;
+ $this->listeners = $listeners;
+ $this->sorted = array_fill_keys(array_keys($this->listeners), TRUE);
+ }
+
+ /**
+ * {@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->sorted[$event_name])) {
+ krsort($this->listeners[$event_name]);
+ $this->sorted[$event_name] = TRUE;
+ }
+
+ // 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'] = [$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 = [];
+
+ 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->sorted[$event_name])) {
+ krsort($this->listeners[$event_name]);
+ $this->sorted[$event_name] = TRUE;
+ }
+
+ // 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'] = [$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][] = ['callable' => $listener];
+ unset($this->sorted[$event_name]);
+ }
+
+ /**
+ * {@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'] = [$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]));
+ }
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterKernelListenersPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterKernelListenersPass.php
index 59a3591..33b2683 100644
--- a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterKernelListenersPass.php
+++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterKernelListenersPass.php
@@ -18,6 +18,7 @@ public function process(ContainerBuilder $container) {
$definition = $container->getDefinition('event_dispatcher');
+ $event_subscriber_info = [];
foreach ($container->findTaggedServiceIds('event_subscriber') as $id => $attributes) {
// We must assume that the class value has been correctly filled, even if the service is created by a factory
@@ -28,7 +29,30 @@ public function process(ContainerBuilder $container) {
if (!$refClass->implementsInterface($interface)) {
throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
}
- $definition->addMethodCall('addSubscriberService', array($id, $class));
+
+ // Get all subscribed events.
+ foreach ($class::getSubscribedEvents() as $event_name => $params) {
+ if (is_string($params)) {
+ $priority = 0;
+ $event_subscriber_info[$event_name][$priority][] = ['service' => [$id, $params]];
+ }
+ elseif (is_string($params[0])) {
+ $priority = isset($params[1]) ? $params[1] : 0;
+ $event_subscriber_info[$event_name][$priority][] = ['service' => [$id, $params[0]]];
+ }
+ else {
+ foreach ($params as $listener) {
+ $priority = isset($listener[1]) ? $listener[1] : 0;
+ $event_subscriber_info[$event_name][$priority][] = ['service' => [$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/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php b/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php
new file mode 100644
index 0000000..c518e75
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php
@@ -0,0 +1,375 @@
+dispatcher = new ContainerAwareEventDispatcher(new Container());
+ $this->listener = new TestEventListener();
+ }
+
+ protected function tearDown()
+ {
+ $this->dispatcher = null;
+ $this->listener = null;
+ }
+
+ public function testInitialState()
+ {
+ $this->assertEquals(array(), $this->dispatcher->getListeners());
+ $this->assertFalse($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertFalse($this->dispatcher->hasListeners(self::postFoo));
+ }
+
+ public function testAddListener()
+ {
+ $this->dispatcher->addListener('pre.foo', array($this->listener, 'preFoo'));
+ $this->dispatcher->addListener('post.foo', array($this->listener, 'postFoo'));
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertTrue($this->dispatcher->hasListeners(self::postFoo));
+ $this->assertCount(1, $this->dispatcher->getListeners(self::preFoo));
+ $this->assertCount(1, $this->dispatcher->getListeners(self::postFoo));
+ $this->assertCount(2, $this->dispatcher->getListeners());
+ }
+
+ public function testGetListenersSortsByPriority()
+ {
+ $listener1 = new TestEventListener();
+ $listener2 = new TestEventListener();
+ $listener3 = new TestEventListener();
+ $listener1->name = '1';
+ $listener2->name = '2';
+ $listener3->name = '3';
+
+ $this->dispatcher->addListener('pre.foo', array($listener1, 'preFoo'), -10);
+ $this->dispatcher->addListener('pre.foo', array($listener2, 'preFoo'), 10);
+ $this->dispatcher->addListener('pre.foo', array($listener3, 'preFoo'));
+
+ $expected = array(
+ array($listener2, 'preFoo'),
+ array($listener3, 'preFoo'),
+ array($listener1, 'preFoo'),
+ );
+
+ $this->assertSame($expected, $this->dispatcher->getListeners('pre.foo'));
+ }
+
+ public function testGetAllListenersSortsByPriority()
+ {
+ $listener1 = new TestEventListener();
+ $listener2 = new TestEventListener();
+ $listener3 = new TestEventListener();
+ $listener4 = new TestEventListener();
+ $listener5 = new TestEventListener();
+ $listener6 = new TestEventListener();
+
+ $this->dispatcher->addListener('pre.foo', $listener1, -10);
+ $this->dispatcher->addListener('pre.foo', $listener2);
+ $this->dispatcher->addListener('pre.foo', $listener3, 10);
+ $this->dispatcher->addListener('post.foo', $listener4, -10);
+ $this->dispatcher->addListener('post.foo', $listener5);
+ $this->dispatcher->addListener('post.foo', $listener6, 10);
+
+ $expected = array(
+ 'pre.foo' => array($listener3, $listener2, $listener1),
+ 'post.foo' => array($listener6, $listener5, $listener4),
+ );
+
+ $this->assertSame($expected, $this->dispatcher->getListeners());
+ }
+
+ public function testDispatch()
+ {
+ $this->dispatcher->addListener('pre.foo', array($this->listener, 'preFoo'));
+ $this->dispatcher->addListener('post.foo', array($this->listener, 'postFoo'));
+ $this->dispatcher->dispatch(self::preFoo);
+ $this->assertTrue($this->listener->preFooInvoked);
+ $this->assertFalse($this->listener->postFooInvoked);
+ $this->assertInstanceOf('Symfony\Component\EventDispatcher\Event', $this->dispatcher->dispatch('noevent'));
+ $this->assertInstanceOf('Symfony\Component\EventDispatcher\Event', $this->dispatcher->dispatch(self::preFoo));
+ $event = new Event();
+ $return = $this->dispatcher->dispatch(self::preFoo, $event);
+ $this->assertEquals('pre.foo', $event->getName());
+ $this->assertSame($event, $return);
+ }
+
+ public function testDispatchForClosure()
+ {
+ $invoked = 0;
+ $listener = function () use (&$invoked) {
+ $invoked++;
+ };
+ $this->dispatcher->addListener('pre.foo', $listener);
+ $this->dispatcher->addListener('post.foo', $listener);
+ $this->dispatcher->dispatch(self::preFoo);
+ $this->assertEquals(1, $invoked);
+ }
+
+ public function testStopEventPropagation()
+ {
+ $otherListener = new TestEventListener();
+
+ // postFoo() stops the propagation, so only one listener should
+ // be executed
+ // Manually set priority to enforce $this->listener to be called first
+ $this->dispatcher->addListener('post.foo', array($this->listener, 'postFoo'), 10);
+ $this->dispatcher->addListener('post.foo', array($otherListener, 'preFoo'));
+ $this->dispatcher->dispatch(self::postFoo);
+ $this->assertTrue($this->listener->postFooInvoked);
+ $this->assertFalse($otherListener->postFooInvoked);
+ }
+
+ public function testDispatchByPriority()
+ {
+ $invoked = array();
+ $listener1 = function () use (&$invoked) {
+ $invoked[] = '1';
+ };
+ $listener2 = function () use (&$invoked) {
+ $invoked[] = '2';
+ };
+ $listener3 = function () use (&$invoked) {
+ $invoked[] = '3';
+ };
+ $this->dispatcher->addListener('pre.foo', $listener1, -10);
+ $this->dispatcher->addListener('pre.foo', $listener2);
+ $this->dispatcher->addListener('pre.foo', $listener3, 10);
+ $this->dispatcher->dispatch(self::preFoo);
+ $this->assertEquals(array('3', '2', '1'), $invoked);
+ }
+
+ public function testRemoveListener()
+ {
+ $this->dispatcher->addListener('pre.bar', $this->listener);
+ $this->assertTrue($this->dispatcher->hasListeners(self::preBar));
+ $this->dispatcher->removeListener('pre.bar', $this->listener);
+ $this->assertFalse($this->dispatcher->hasListeners(self::preBar));
+ $this->dispatcher->removeListener('notExists', $this->listener);
+ }
+
+ public function testAddSubscriber()
+ {
+ $eventSubscriber = new TestEventSubscriber();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertTrue($this->dispatcher->hasListeners(self::postFoo));
+ }
+
+ public function testAddSubscriberWithPriorities()
+ {
+ $eventSubscriber = new TestEventSubscriber();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+
+ $eventSubscriber = new TestEventSubscriberWithPriorities();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+
+ $listeners = $this->dispatcher->getListeners('pre.foo');
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertCount(2, $listeners);
+ $this->assertInstanceOf('Drupal\Tests\Component\EventDispatcher\TestEventSubscriberWithPriorities', $listeners[0][0]);
+ }
+
+ public function testAddSubscriberWithMultipleListeners()
+ {
+ $eventSubscriber = new TestEventSubscriberWithMultipleListeners();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+
+ $listeners = $this->dispatcher->getListeners('pre.foo');
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertCount(2, $listeners);
+ $this->assertEquals('preFoo2', $listeners[0][1]);
+ }
+
+ public function testRemoveSubscriber()
+ {
+ $eventSubscriber = new TestEventSubscriber();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertTrue($this->dispatcher->hasListeners(self::postFoo));
+ $this->dispatcher->removeSubscriber($eventSubscriber);
+ $this->assertFalse($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertFalse($this->dispatcher->hasListeners(self::postFoo));
+ }
+
+ public function testRemoveSubscriberWithPriorities()
+ {
+ $eventSubscriber = new TestEventSubscriberWithPriorities();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->dispatcher->removeSubscriber($eventSubscriber);
+ $this->assertFalse($this->dispatcher->hasListeners(self::preFoo));
+ }
+
+ public function testRemoveSubscriberWithMultipleListeners()
+ {
+ $eventSubscriber = new TestEventSubscriberWithMultipleListeners();
+ $this->dispatcher->addSubscriber($eventSubscriber);
+ $this->assertTrue($this->dispatcher->hasListeners(self::preFoo));
+ $this->assertCount(2, $this->dispatcher->getListeners(self::preFoo));
+ $this->dispatcher->removeSubscriber($eventSubscriber);
+ $this->assertFalse($this->dispatcher->hasListeners(self::preFoo));
+ }
+
+ public function testEventReceivesTheDispatcherInstance()
+ {
+ $dispatcher = null;
+ $this->dispatcher->addListener('test', function ($event) use (&$dispatcher) {
+ $dispatcher = $event->getDispatcher();
+ });
+ $this->dispatcher->dispatch('test');
+ $this->assertSame($this->dispatcher, $dispatcher);
+ }
+
+ public function testEventReceivesTheDispatcherInstanceAsArgument()
+ {
+ $listener = new TestWithDispatcher();
+ $this->dispatcher->addListener('test', array($listener, 'foo'));
+ $this->assertNull($listener->name);
+ $this->assertNull($listener->dispatcher);
+ $this->dispatcher->dispatch('test');
+ $this->assertEquals('test', $listener->name);
+ $this->assertSame($this->dispatcher, $listener->dispatcher);
+ }
+
+ /**
+ * @see https://bugs.php.net/bug.php?id=62976
+ *
+ * This bug affects:
+ * - The PHP 5.3 branch for versions < 5.3.18
+ * - The PHP 5.4 branch for versions < 5.4.8
+ * - The PHP 5.5 branch is not affected
+ */
+ public function testWorkaroundForPhpBug62976()
+ {
+ $dispatcher = new ContainerAwareEventDispatcher(new Container());
+ $dispatcher->addListener('bug.62976', new CallableClass());
+ $dispatcher->removeListener('bug.62976', function () {});
+ $this->assertTrue($dispatcher->hasListeners('bug.62976'));
+ }
+
+ public function testHasListenersWhenAddedCallbackListenerIsRemoved()
+ {
+ $listener = function () {};
+ $this->dispatcher->addListener('foo', $listener);
+ $this->dispatcher->removeListener('foo', $listener);
+ $this->assertFalse($this->dispatcher->hasListeners());
+ }
+
+ public function testGetListenersWhenAddedCallbackListenerIsRemoved()
+ {
+ $listener = function () {};
+ $this->dispatcher->addListener('foo', $listener);
+ $this->dispatcher->removeListener('foo', $listener);
+ $this->assertSame(array(), $this->dispatcher->getListeners());
+ }
+
+ public function testHasListenersWithoutEventsReturnsFalseAfterHasListenersWithEventHasBeenCalled()
+ {
+ $this->assertFalse($this->dispatcher->hasListeners('foo'));
+ $this->assertFalse($this->dispatcher->hasListeners());
+ }
+}
+
+class CallableClass
+{
+ public function __invoke()
+ {
+ }
+}
+
+class TestEventListener
+{
+ public $preFooInvoked = false;
+ public $postFooInvoked = false;
+
+ /* Listener methods */
+
+ public function preFoo(Event $e)
+ {
+ $this->preFooInvoked = true;
+ }
+
+ public function postFoo(Event $e)
+ {
+ $this->postFooInvoked = true;
+
+ $e->stopPropagation();
+ }
+}
+
+class TestWithDispatcher
+{
+ public $name;
+ public $dispatcher;
+
+ public function foo(Event $e, $name, $dispatcher)
+ {
+ $this->name = $name;
+ $this->dispatcher = $dispatcher;
+ }
+}
+
+class TestEventSubscriber implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array('pre.foo' => 'preFoo', 'post.foo' => 'postFoo');
+ }
+}
+
+class TestEventSubscriberWithPriorities implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array(
+ 'pre.foo' => array('preFoo', 10),
+ 'post.foo' => array('postFoo'),
+ );
+ }
+}
+
+class TestEventSubscriberWithMultipleListeners implements EventSubscriberInterface
+{
+ public static function getSubscribedEvents()
+ {
+ return array('pre.foo' => array(
+ array('preFoo1'),
+ array('preFoo2', 10),
+ ));
+ }
+}