class HookCollectorPass

Collects and registers hook implementations.

A hook implementation is a class in a Drupal\modulename\Hook namespace where either the class itself or the methods have a #[Hook] attribute. These classes are automatically registered as autowired services.

Services for procedural implementation of hooks are also registered using the ProceduralCall class.

Finally, a hook_implementations_map container parameter is added. This contains a mapping from [hook,class,method] to the module name.

Hierarchy

  • class \Drupal\Core\Hook\HookCollectorPass implements \Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface

Expanded class hierarchy of HookCollectorPass

4 files declare their use of HookCollectorPass
CoreServiceProvider.php in core/lib/Drupal/Core/CoreServiceProvider.php
HookCollectorPassTest.php in core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php
HookCollectorPassTest.php in core/tests/Drupal/Tests/Core/Hook/HookCollectorPassTest.php
ModuleHandler.php in core/lib/Drupal/Core/Extension/ModuleHandler.php

File

core/lib/Drupal/Core/Hook/HookCollectorPass.php, line 28

Namespace

Drupal\Core\Hook
View source
class HookCollectorPass implements CompilerPassInterface {
    
    /**
     * An associative array of hook implementations.
     *
     * Keys are hook, module, class. Values are a list of methods.
     */
    protected array $implementations = [];
    
    /**
     * An associative array of hook implementations.
     *
     * Keys are hook, module and an empty string value.
     *
     * @see hook_module_implements_alter()
     */
    protected array $moduleImplements = [];
    
    /**
     * A list of include files.
     *
     * (This is required only for BC.)
     */
    protected array $includes = [];
    
    /**
     * A list of functions implementing hook_module_implements_alter().
     *
     * (This is required only for BC.)
     */
    protected array $moduleImplementsAlters = [];
    
    /**
     * A list of functions implementing hook_hook_info().
     *
     * (This is required only for BC.)
     */
    private array $hookInfo = [];
    
    /**
     * A list of .inc files.
     */
    private array $groupIncludes = [];
    
    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container) : void {
        $collector = static::collectAllHookImplementations($container->getParameter('container.modules'));
        $map = [];
        $container->register(ProceduralCall::class, ProceduralCall::class)
            ->addArgument($collector->includes);
        $groupIncludes = [];
        foreach ($collector->hookInfo as $function) {
            foreach ($function() as $hook => $info) {
                if (isset($collector->groupIncludes[$info['group']])) {
                    $groupIncludes[$hook] = $collector->groupIncludes[$info['group']];
                }
            }
        }
        $definition = $container->getDefinition('module_handler');
        $definition->setArgument('$groupIncludes', $groupIncludes);
        foreach ($collector->moduleImplements as $hook => $moduleImplements) {
            foreach ($collector->moduleImplementsAlters as $alter) {
                $alter($moduleImplements, $hook);
            }
            $priority = 0;
            foreach ($moduleImplements as $module => $v) {
                foreach ($collector->implementations[$hook][$module] as $class => $method_hooks) {
                    if ($container->has($class)) {
                        $definition = $container->findDefinition($class);
                    }
                    else {
                        $definition = $container->register($class, $class)
                            ->setAutowired(TRUE);
                    }
                    foreach ($method_hooks as $method) {
                        $map[$hook][$class][$method] = $module;
                        $definition->addTag('kernel.event_listener', [
                            'event' => "drupal_hook.{$hook}",
                            'method' => $method,
                            'priority' => $priority--,
                        ]);
                    }
                }
            }
        }
        $container->setParameter('hook_implementations_map', $map);
    }
    
    /**
     * Collects all hook implementations.
     *
     * @param array $module_filenames
     *   An associative array. Keys are the module names, values are relevant
     *   info yml file path.
     *
     * @return static
     *   A HookCollectorPass instance holding all hook implementations and
     *   include file information.
     *
     * @internal
     *   This method is only used by ModuleHandler.
     */
    public static function collectAllHookImplementations(array $module_filenames) : static {
        $modules = array_map(fn($x) => preg_quote($x, '/'), array_keys($module_filenames));
        // Longer modules first.
        usort($modules, fn($a, $b) => strlen($b) - strlen($a));
        $module_preg = '/^(?<function>(?<module>' . implode('|', $modules) . ')_(?!preprocess_)(?!update_\\d)(?<hook>[a-zA-Z0-9_\\x80-\\xff]+$))/';
        $collector = new static();
        foreach ($module_filenames as $module => $info) {
            $collector->collectModuleHookImplementations(dirname($info['pathname']), $module, $module_preg);
        }
        return $collector;
    }
    
    /**
     * Collects procedural and Attribute hook implementations.
     *
     * @param $dir
     *   The directory in which the module resides.
     * @param $module
     *   The name of the module.
     * @param $module_preg
     *   A regular expression matching every module, longer module names are
     *   matched first.
     *
     * @return void
     */
    protected function collectModuleHookImplementations($dir, $module, $module_preg) : void {
        $iterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::FOLLOW_SYMLINKS);
        $iterator = new \RecursiveCallbackFilterIterator($iterator, static::filterIterator(...));
        $iterator = new \RecursiveIteratorIterator($iterator);
        
        /** @var \RecursiveDirectoryIterator | \RecursiveIteratorIterator $iterator*/
        foreach ($iterator as $fileinfo) {
            assert($fileinfo instanceof \SplFileInfo);
            $extension = $fileinfo->getExtension();
            if ($extension === 'module' && !$iterator->getDepth()) {
                // There is an expectation for all modules to be loaded. However,
                // .module files are not supposed to be in subdirectories.
                include_once $fileinfo->getPathname();
            }
            if ($extension === 'php') {
                $namespace = preg_replace('#^src/#', "Drupal/{$module}/", $iterator->getSubPath());
                $class = $namespace . '/' . $fileinfo->getBasename('.php');
                $class = str_replace('/', '\\', $class);
                foreach (static::getHookAttributesInClass($class) as $attribute) {
                    $this->addFromAttribute($attribute, $class, $module);
                }
            }
            else {
                $finder = MockFileFinder::create($fileinfo->getPathName());
                $parser = new StaticReflectionParser('', $finder);
                foreach ($parser->getMethodAttributes() as $function => $attributes) {
                    if (!StaticReflectionParser::hasAttribute($attributes, LegacyHook::class) && preg_match($module_preg, $function, $matches)) {
                        $this->addProceduralImplementation($fileinfo, $matches['hook'], $matches['module'], $matches['function']);
                    }
                }
            }
            if ($extension === 'inc') {
                $parts = explode('.', $fileinfo->getFilename());
                if (count($parts) === 3 && $parts[0] === $module) {
                    $this->groupIncludes[$parts[1]][] = $fileinfo->getPathname();
                }
            }
        }
    }
    
    /**
     * Filter iterator callback. Allows include files and .php files in src/Hook.
     */
    protected static function filterIterator(\SplFileInfo $fileInfo, $key, \RecursiveDirectoryIterator $iterator) : bool {
        $sub_path_name = $iterator->getSubPathname();
        $extension = $fileInfo->getExtension();
        if (str_starts_with($sub_path_name, 'src/Hook/')) {
            return $iterator->isDir() || $extension === 'php';
        }
        if ($iterator->isDir()) {
            if ($sub_path_name === 'src' || $sub_path_name === 'src/Hook') {
                return TRUE;
            }
            // glob() doesn't support streams but scandir() does.
            return !in_array($fileInfo->getFilename(), [
                'tests',
                'js',
                'css',
            ]) && !array_filter(scandir($key), fn($filename) => str_ends_with($filename, '.info.yml'));
        }
        return in_array($extension, [
            'inc',
            'module',
            'profile',
            'install',
        ]);
    }
    
    /**
     * An array of Hook attributes on this class with $method set.
     *
     * @param string $class
     *   The class.
     *
     * @return \Drupal\Core\Hook\Attribute\Hook[]
     *   An array of Hook attributes on this class. The $method property is guaranteed to be set.
     */
    protected static function getHookAttributesInClass(string $class) : array {
        if (!class_exists($class)) {
            return [];
        }
        $reflection_class = new \ReflectionClass($class);
        $class_implementations = [];
        // Check for #[Hook] on the class itself.
        foreach ($reflection_class->getAttributes(Hook::class, \ReflectionAttribute::IS_INSTANCEOF) as $reflection_attribute) {
            $hook = $reflection_attribute->newInstance();
            assert($hook instanceof Hook);
            self::checkForProceduralOnlyHooks($hook, $class);
            if (!$hook->method) {
                if (method_exists($class, '__invoke')) {
                    $hook->setMethod('__invoke');
                }
                else {
                    throw new \LogicException("The Hook attribute for hook {$hook->hook} on class {$class} must specify a method.");
                }
            }
            $class_implementations[] = $hook;
        }
        // Check for #[Hook] on methods.
        foreach ($reflection_class->getMethods(\ReflectionMethod::IS_PUBLIC) as $method_reflection) {
            foreach ($method_reflection->getAttributes(Hook::class, \ReflectionAttribute::IS_INSTANCEOF) as $attribute_reflection) {
                $hook = $attribute_reflection->newInstance();
                assert($hook instanceof Hook);
                self::checkForProceduralOnlyHooks($hook, $class);
                $class_implementations[] = $hook->setMethod($method_reflection->getName());
            }
        }
        return $class_implementations;
    }
    
    /**
     * Adds a Hook attribute implementation.
     *
     * @param \Drupal\Core\Hook\Attribute\Hook $hook
     *   A hook attribute.
     * @param $class
     *   The class in which said attribute resides in.
     * @param $module
     *   The module in which the class resides in.
     *
     * @return void
     */
    protected function addFromAttribute(Hook $hook, $class, $module) {
        if ($hook->module) {
            $module = $hook->module;
        }
        $this->moduleImplements[$hook->hook][$module] = '';
        $this->implementations[$hook->hook][$module][$class][] = $hook->method;
    }
    
    /**
     * Adds a procedural hook implementation.
     *
     * @param \SplFileInfo $fileinfo
     *   The file this procedural implementation is in. (You don't say)
     * @param string $hook
     *   The name of the hook. (Huh, right?)
     * @param string $module
     *   The name of the module. (Truly shocking!)
     * @param string $function
     *   The name of function implementing the hook. (Wow!)
     *
     * @return void
     */
    protected function addProceduralImplementation(\SplFileInfo $fileinfo, string $hook, string $module, string $function) {
        $this->addFromAttribute(new Hook($hook, $module . '_' . $hook), ProceduralCall::class, $module);
        if ($hook === 'hook_info') {
            $this->hookInfo[] = $function;
        }
        if ($hook === 'module_implements_alter') {
            $this->moduleImplementsAlters[] = $function;
        }
        if ($fileinfo->getExtension() !== 'module') {
            $this->includes[$function] = $fileinfo->getPathname();
        }
    }
    
    /**
     * This method is only to be used by ModuleHandler.
     *
     * @internal
     */
    public function loadAllIncludes() : void {
        foreach ($this->includes as $include) {
            include_once $include;
        }
    }
    
    /**
     * This method is only to be used by ModuleHandler.
     *
     * @internal
     */
    public function getImplementations() : array {
        return $this->implementations;
    }
    
    /**
     * Checks for hooks which can't be supported in classes.
     *
     * @param \Drupal\Core\Hook\Attribute\Hook $hook
     *   The hook to check.
     * @param string $class
     *   The class the hook is implemented on.
     *
     * @return void
     */
    public static function checkForProceduralOnlyHooks(Hook $hook, string $class) : void {
        $staticDenyHooks = [
            'hook_info',
            'install',
            'module_implements_alter',
            'requirements',
            'schema',
            'uninstall',
            'update_last_removed',
        ];
        if (in_array($hook->hook, $staticDenyHooks) || preg_match('/^(post_update_|preprocess_|process_|update_\\d+$)/', $hook->hook)) {
            throw new \LogicException("The hook {$hook->hook} on class {$class} does not support attributes and must remain procedural.");
        }
    }

}

Members

Title Sort descending Modifiers Object type Summary
HookCollectorPass::$groupIncludes private property A list of .inc files.
HookCollectorPass::$hookInfo private property A list of functions implementing hook_hook_info().
HookCollectorPass::$implementations protected property An associative array of hook implementations.
HookCollectorPass::$includes protected property A list of include files.
HookCollectorPass::$moduleImplementsAlters protected property A list of functions implementing hook_module_implements_alter().
HookCollectorPass::addFromAttribute protected function Adds a Hook attribute implementation.
HookCollectorPass::addProceduralImplementation protected function Adds a procedural hook implementation.
HookCollectorPass::checkForProceduralOnlyHooks public static function Checks for hooks which can&#039;t be supported in classes.
HookCollectorPass::collectAllHookImplementations public static function Collects all hook implementations.
HookCollectorPass::collectModuleHookImplementations protected function Collects procedural and Attribute hook implementations.
HookCollectorPass::filterIterator protected static function Filter iterator callback. Allows include files and .php files in src/Hook.
HookCollectorPass::getHookAttributesInClass protected static function An array of Hook attributes on this class with $method set.
HookCollectorPass::getImplementations public function This method is only to be used by ModuleHandler.
HookCollectorPass::loadAllIncludes public function This method is only to be used by ModuleHandler.
HookCollectorPass::process public function

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