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), + )); + } +}