class HookOrder

Helper methods to set priorities of hook implementations.

Hierarchy

Expanded class hierarchy of HookOrder

2 files declare their use of HookOrder
HookOrderEqualPriorityTest.php in core/tests/Drupal/Tests/Core/Hook/HookOrderEqualPriorityTest.php
HookOrderTest.php in core/tests/Drupal/Tests/Core/Hook/HookOrderTest.php

File

core/lib/Drupal/Core/Hook/HookOrder.php, line 12

Namespace

Drupal\Core\Hook
View source
class HookOrder {
    
    /**
     * Set a hook implementation to be first.
     *
     * @param \Drupal\Core\DependencyInjection\ContainerBuilder $container
     *   The container builder.
     * @param string $hook
     *   The name of the hook.
     * @param string $class_and_method
     *   Class and method separated by :: containing the hook implementation.
     *
     * @return void
     */
    public static function first(ContainerBuilder $container, string $hook, string $class_and_method) : void {
        self::changePriority($container, $hook, $class_and_method, TRUE);
    }
    
    /**
     * Set a hook implementation to be last.
     *
     * @param \Drupal\Core\DependencyInjection\ContainerBuilder $container
     *   The container builder.
     * @param string $hook
     *   The name of the hook.
     * @param string $class_and_method
     *   Class and method separated by :: containing the hook implementation.
     *
     * @return void
     */
    public static function last(ContainerBuilder $container, string $hook, string $class_and_method) : void {
        self::changePriority($container, $hook, $class_and_method, FALSE);
    }
    
    /**
     * Set a hook implementation to fire before others.
     *
     * This method tries to keep existing order as much as possible. For
     * example, if there are five hook implementations, A, B, C, D, E firing in
     * this order then moving D before B will set it after A.
     *
     * @param \Drupal\Core\DependencyInjection\ContainerBuilder $container
     *   The container builder.
     * @param string $hook
     *   The name of the hook.
     * @param string $class_and_method
     *   Class and method separated by :: containing the hook implementation which
     *   should be changed.
     * @param string ...$others
     *   A list specifying the other implementations this hook should fire
     *   before. Every list member is a class and method separated by ::.
     *
     * @return void
     */
    public static function before(ContainerBuilder $container, string $hook, string $class_and_method, string ...$others) : void {
        self::changePriority($container, $hook, $class_and_method, TRUE, array_flip($others));
    }
    
    /**
     * Set a hook implementation to fire after others.
     *
     * This method tries to keep existing order as much as possible. For
     * example, if there are five hook implementations, A, B, C, D, E firing in
     * this order then moving B after D will set it before E.
     *
     * @param \Drupal\Core\DependencyInjection\ContainerBuilder $container
     *   The container builder.
     * @param string $hook
     *   The name of the hook.
     * @param string $class_and_method
     *   Class and method separated by :: containing the hook implementation which
     *   should be changed.
     * @param string ...$others
     *   A list specifying the other implementations this hook should fire
     *   before. Every list member is a class and method separated by ::.
     *
     * @return void
     */
    public static function after(ContainerBuilder $container, string $hook, string $class_and_method, string ...$others) : void {
        self::changePriority($container, $hook, $class_and_method, FALSE, array_flip($others));
    }
    
    /**
     * Change the priority of a hook implementation.
     *
     * @param \Drupal\Core\DependencyInjection\ContainerBuilder $container
     *   The container builder.
     * @param string $hook
     *   The name of the hook.
     * @param string $class_and_method
     *   Class and method separated by :: containing the hook implementation which
     *   should be changed.
     * @param bool $should_be_larger
     *   TRUE for before/first, FALSE for after/last. Larger priority listeners
     *   fire first.
     * @param array|null $others
     *   Other hook implementations to compare to, if any. The array is keyed by
     *   string containing a class and method separated by ::, the value is not
     *   used.
     *
     * @return void
     */
    protected static function changePriority(ContainerBuilder $container, string $hook, string $class_and_method, bool $should_be_larger, ?array $others = NULL) : void {
        $event = "drupal_hook.{$hook}";
        foreach ($container->findTaggedServiceIds('kernel.event_listener') as $id => $attributes) {
            foreach ($attributes as $key => $tag) {
                if ($tag['event'] === $event) {
                    $index = "{$id}.{$key}";
                    $priority = $tag['priority'];
                    // Symfony documents event listener priorities to be integers,
                    // HookCollectorPass sets them to be integers, ::setPriority() only
                    // accepts integers.
                    assert(is_int($priority));
                    $priorities[$index] = $priority;
                    $specifier = "{$id}::" . $tag['method'];
                    if ($class_and_method === $specifier) {
                        $index_this = $index;
                    }
                    elseif (!isset($others) || isset($others[$specifier])) {
                        $priorities_other[] = $priority;
                    }
                }
            }
        }
        if (!isset($index_this) || !isset($priorities) || !isset($priorities_other)) {
            return;
        }
        // The priority of the hook being changed.
        $priority_this = $priorities[$index_this];
        // The priority of the hook being compared to.
        $priority_other = $should_be_larger ? max($priorities_other) : min($priorities_other);
        // If the order is correct there is nothing to do. If the two priorities
        // are the same then the order is undefined and so it can't be correct.
        // If they are not the same and $priority_this is already larger exactly
        // when $should_be_larger says then it's the correct order.
        if ($priority_this !== $priority_other && $should_be_larger === $priority_this > $priority_other) {
            return;
        }
        $priority_new = $priority_other + ($should_be_larger ? 1 : -1);
        // For ::first() / ::last() this new priority is already larger/smaller
        // than all existing priorities but for ::before() / ::after() it might
        // belong to an already existing hook. In this case set the new priority
        // temporarily to be halfway between $priority_other and $priority_new
        // then give all hook implementations new, integer priorities keeping this
        // new order. This ensures the hook implementation being changed is in the
        // right order relative to both $priority_other and the hook whose
        // priority was $priority_new.
        if (in_array($priority_new, $priorities)) {
            $priorities[$index_this] = $priority_other + ($should_be_larger ? 0.5 : -0.5);
            asort($priorities);
            $changed_indexes = array_keys($priorities);
            $priorities = array_combine($changed_indexes, range(1, count($changed_indexes)));
        }
        else {
            $priorities[$index_this] = $priority_new;
            $changed_indexes = [
                $index_this,
            ];
        }
        foreach ($changed_indexes as $index) {
            [
                $id,
                $key,
            ] = explode('.', $index);
            self::setPriority($container, $id, (int) $key, $priorities[$index]);
        }
    }
    
    /**
     * Set the priority of a listener.
     *
     * @param \Drupal\Core\DependencyInjection\ContainerBuilder $container
     *   The container.
     * @param string $class
     *   The name of the class, this is the same as the service id.
     * @param int $key
     *   The key within the tags array of the 'kernel.event_listener' tag for the
     *   hook implementation to be changed.
     * @param int $priority
     *   The new priority.
     *
     * @return void
     */
    public static function setPriority(ContainerBuilder $container, string $class, int $key, int $priority) : void {
        $definition = $container->getDefinition($class);
        $tags = $definition->getTags();
        $tags['kernel.event_listener'][$key]['priority'] = $priority;
        $definition->setTags($tags);
    }

}

Members

Title Sort descending Modifiers Object type Summary
HookOrder::after public static function Set a hook implementation to fire after others.
HookOrder::before public static function Set a hook implementation to fire before others.
HookOrder::changePriority protected static function Change the priority of a hook implementation.
HookOrder::first public static function Set a hook implementation to be first.
HookOrder::last public static function Set a hook implementation to be last.
HookOrder::setPriority public static function Set the priority of a listener.

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