class ContainerAwareEventDispatcher

Same name in other branches
  1. 9 core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher
  2. 8.9.x core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher
  3. 11.x core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher

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:

<dl> <dt>Faster instantiation of the event dispatcher service</dt> <dd> Instead of calling <code>addSubscriberService</code> once for each subscriber, a pre-compiled 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. </dd> <dt>Lazy instantiation of listeners</dt> <dd> 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. </dd> </dl>

Hierarchy

Expanded class hierarchy of ContainerAwareEventDispatcher

3 files declare their use of ContainerAwareEventDispatcher
ContainerAwareEventDispatcherTest.php in core/tests/Drupal/Tests/Component/EventDispatcher/ContainerAwareEventDispatcherTest.php
RedirectResponseSubscriberTest.php in core/tests/Drupal/Tests/Core/EventSubscriber/RedirectResponseSubscriberTest.php
SectionRenderTest.php in core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php
1 string reference to 'ContainerAwareEventDispatcher'
core.services.yml in core/core.services.yml
core/core.services.yml
1 service uses ContainerAwareEventDispatcher
event_dispatcher in core/core.services.yml
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher

File

core/lib/Drupal/Component/EventDispatcher/ContainerAwareEventDispatcher.php, line 34

Namespace

Drupal\Component\EventDispatcher
View source
class ContainerAwareEventDispatcher implements EventDispatcherInterface {
    
    /**
     * The service container.
     *
     * @var \Symfony\Component\DependencyInjection\ContainerInterface
     */
    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 listeners need to be sorted prior to dispatch, keyed by event name.
     *
     * @var TRUE[]
     */
    protected $unsorted;
    
    /**
     * Constructs a container aware event dispatcher.
     *
     * @param \Symfony\Component\DependencyInjection\ContainerInterface $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(ContainerInterface $container, array $listeners = []) {
        $this->container = $container;
        $this->listeners = $listeners;
        $this->unsorted = [];
    }
    
    /**
     * {@inheritdoc}
     */
    public function dispatch(object $event, ?string $eventName = NULL) : object {
        $event_name = $eventName ?? get_class($event);
        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]);
            }
            $stoppable = $event instanceof StoppableEventInterface;
            // Invoke listeners and resolve callables if necessary.
            foreach ($this->listeners[$event_name] as &$definitions) {
                foreach ($definitions as &$definition) {
                    if (!isset($definition['callable'])) {
                        $definition['callable'] = [
                            $this->container
                                ->get($definition['service'][0]),
                            $definition['service'][1],
                        ];
                    }
                    if (is_array($definition['callable']) && isset($definition['callable'][0]) && $definition['callable'][0] instanceof \Closure) {
                        $definition['callable'][0] = $definition['callable'][0]();
                    }
                    call_user_func($definition['callable'], $event, $event_name, $this);
                    if ($stoppable && $event->isPropagationStopped()) {
                        return $event;
                    }
                }
            }
        }
        return $event;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getListeners($event_name = NULL) : array {
        $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->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 &$definitions) {
                foreach ($definitions as &$definition) {
                    if (!isset($definition['callable'])) {
                        $definition['callable'] = [
                            $this->container
                                ->get($definition['service'][0]),
                            $definition['service'][1],
                        ];
                    }
                    if (is_array($definition['callable']) && isset($definition['callable'][0]) && $definition['callable'][0] instanceof \Closure) {
                        $definition['callable'][0] = $definition['callable'][0]();
                    }
                    $result[] = $definition['callable'];
                }
            }
        }
        return $result;
    }
    
    /**
     * {@inheritdoc}
     */
    public function getListenerPriority($event_name, $listener) : ?int {
        if (!isset($this->listeners[$event_name])) {
            return NULL;
        }
        if (is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure) {
            $listener[0] = $listener[0]();
        }
        // Resolve service definitions if the listener has not been found so far.
        foreach ($this->listeners[$event_name] as $priority => &$definitions) {
            foreach ($definitions as &$definition) {
                if (!isset($definition['callable'])) {
                    // Once the callable is retrieved we keep it for subsequent method
                    // invocations on this class.
                    $definition['callable'] = [
                        $this->container
                            ->get($definition['service'][0]),
                        $definition['service'][1],
                    ];
                }
                if (is_array($definition['callable']) && isset($definition['callable'][0]) && $definition['callable'][0] instanceof \Closure) {
                    $definition['callable'][0] = $definition['callable'][0]();
                }
                if ($definition['callable'] === $listener) {
                    return $priority;
                }
            }
        }
        return NULL;
    }
    
    /**
     * {@inheritdoc}
     */
    public function hasListeners($event_name = NULL) : bool {
        if ($event_name !== NULL) {
            return !empty($this->listeners[$event_name]);
        }
        foreach ($this->listeners as $event_listeners) {
            if ($event_listeners) {
                return TRUE;
            }
        }
        return FALSE;
    }
    
    /**
     * {@inheritdoc}
     *
     * phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
     * @return void
     */
    public function addListener($event_name, $listener, $priority = 0) {
        $this->listeners[$event_name][$priority][] = [
            'callable' => $listener,
        ];
        $this->unsorted[$event_name] = TRUE;
    }
    
    /**
     * {@inheritdoc}
     *
     * phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
     * @return void
     */
    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 (is_array($definition['callable']) && isset($definition['callable'][0]) && $definition['callable'][0] instanceof \Closure && !$listener instanceof \Closure) {
                    $definition['callable'][0] = $definition['callable'][0]();
                }
                if (is_array($definition['callable']) && isset($definition['callable'][0]) && !$definition['callable'][0] instanceof \Closure && is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure) {
                    $listener[0] = $listener[0]();
                }
                if ($definition['callable'] === $listener) {
                    unset($definitions[$key]);
                }
            }
            if ($definitions) {
                $this->listeners[$event_name][$priority] = $definitions;
            }
            else {
                unset($this->listeners[$event_name][$priority]);
            }
        }
    }
    
    /**
     * {@inheritdoc}
     *
     * phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
     * @return void
     */
    public function addSubscriber(EventSubscriberInterface $subscriber) {
        foreach ($subscriber->getSubscribedEvents() as $event_name => $params) {
            if (is_string($params)) {
                $this->addListener($event_name, [
                    $subscriber,
                    $params,
                ]);
            }
            elseif (is_string($params[0])) {
                $this->addListener($event_name, [
                    $subscriber,
                    $params[0],
                ], $params[1] ?? 0);
            }
            else {
                foreach ($params as $listener) {
                    $this->addListener($event_name, [
                        $subscriber,
                        $listener[0],
                    ], $listener[1] ?? 0);
                }
            }
        }
    }
    
    /**
     * {@inheritdoc}
     *
     * phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
     * @return void
     */
    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, [
                        $subscriber,
                        $listener[0],
                    ]);
                }
            }
            else {
                $this->removeListener($event_name, [
                    $subscriber,
                    is_string($params) ? $params : $params[0],
                ]);
            }
        }
    }

}

Members

Title Sort descending Modifiers Object type Summary
ContainerAwareEventDispatcher::$container protected property The service container.
ContainerAwareEventDispatcher::$listeners protected property Listener definitions.
ContainerAwareEventDispatcher::$unsorted protected property Whether listeners need to be sorted prior to dispatch, keyed by event name.
ContainerAwareEventDispatcher::addListener public function phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
ContainerAwareEventDispatcher::addSubscriber public function phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
ContainerAwareEventDispatcher::dispatch public function
ContainerAwareEventDispatcher::getListenerPriority public function
ContainerAwareEventDispatcher::getListeners public function
ContainerAwareEventDispatcher::hasListeners public function
ContainerAwareEventDispatcher::removeListener public function phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
ContainerAwareEventDispatcher::removeSubscriber public function phpcs:ignore Drupal.Commenting.FunctionComment.VoidReturn
ContainerAwareEventDispatcher::__construct public function Constructs a container aware event dispatcher.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.