Loading core/lib/Drupal/Core/Hook/Attribute/StopProceduralHookScan.php 0 → 100644 +16 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Core\Hook\Attribute; /** * Defines a StopProceduralHookScan attribute object. * * This allows contrib and core to mark when a file has no more * procedural hooks. */ #[\Attribute(\Attribute::TARGET_FUNCTION)] class StopProceduralHookScan { } core/lib/Drupal/Core/Hook/HookCollectorPass.php +71 −52 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ use Drupal\Core\Extension\ProceduralCall; use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Hook\Attribute\LegacyHook; use Drupal\Core\Hook\Attribute\StopProceduralHookScan; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; Loading Loading @@ -74,7 +75,7 @@ class HookCollectorPass implements CompilerPassInterface { * {@inheritdoc} */ public function process(ContainerBuilder $container): void { $collector = static::collectAllHookImplementations($container->getParameter('container.modules')); $collector = static::collectAllHookImplementations($container->getParameter('container.modules'), $container); $map = []; $container->register(ProceduralCall::class, ProceduralCall::class) ->addArgument($collector->includes); Loading Loading @@ -123,6 +124,8 @@ public function process(ContainerBuilder $container): void { * @param array $module_filenames * An associative array. Keys are the module names, values are relevant * info yml file path. * @param Symfony\Component\DependencyInjection\ContainerBuilder|null $container * The container. * * @return static * A HookCollectorPass instance holding all hook implementations and Loading @@ -130,15 +133,18 @@ public function process(ContainerBuilder $container): void { * * @internal * This method is only used by ModuleHandler. * * * @todo Pass only $container when ModuleHandler->add is removed https://www.drupal.org/project/drupal/issues/3481778 */ public static function collectAllHookImplementations(array $module_filenames): static { public static function collectAllHookImplementations(array $module_filenames, ?ContainerBuilder $container = NULL): 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); $skip_procedural = isset($container) ? $container->hasParameter("$module.hooks_converted") : FALSE; $collector->collectModuleHookImplementations(dirname($info['pathname']), $module, $module_preg, $skip_procedural); } return $collector; } Loading @@ -153,13 +159,21 @@ public static function collectAllHookImplementations(array $module_filenames): s * @param $module_preg * A regular expression matching every module, longer module names are * matched first. * @param $skip_procedural * Skip the procedural check for the current module. * * @return void */ protected function collectModuleHookImplementations($dir, $module, $module_preg): void { protected function collectModuleHookImplementations($dir, $module, $module_preg, bool $skip_procedural): void { $hook_file_cache = FileCacheFactory::get('hook_implementations'); $procedural_hook_file_cache = FileCacheFactory::get('procedural_hook_implementations:' . $module_preg); // Check only hook classes. if ($skip_procedural) { $dir = $dir . '/src/Hook'; } if (is_dir($dir)) { $iterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::FOLLOW_SYMLINKS); $iterator = new \RecursiveCallbackFilterIterator($iterator, static::filterIterator(...)); $iterator = new \RecursiveIteratorIterator($iterator); Loading @@ -169,9 +183,10 @@ protected function collectModuleHookImplementations($dir, $module, $module_preg) $extension = $fileinfo->getExtension(); $filename = $fileinfo->getPathname(); if (($extension === 'module' || $extension === 'profile') && !$iterator->getDepth()) { if (($extension === 'module' || $extension === 'profile') && !$iterator->getDepth() && !$skip_procedural) { // There is an expectation for all modules and profiles to be loaded. // .module and .profile files are not supposed to be in subdirectories. include_once $filename; } if ($extension === 'php') { Loading @@ -196,13 +211,16 @@ protected function collectModuleHookImplementations($dir, $module, $module_preg) $this->addFromAttribute($attribute, $class, $module); } } else { elseif (!$skip_procedural) { $implementations = $procedural_hook_file_cache->get($filename); if ($implementations === NULL) { $finder = MockFileFinder::create($filename); $parser = new StaticReflectionParser('', $finder); $implementations = []; foreach ($parser->getMethodAttributes() as $function => $attributes) { if (StaticReflectionParser::hasAttribute($attributes, StopProceduralHookScan::class)) { break; } if (!StaticReflectionParser::hasAttribute($attributes, LegacyHook::class) && preg_match($module_preg, $function, $matches)) { $implementations[] = ['function' => $function, 'module' => $matches['module'], 'hook' => $matches['hook']]; } Loading @@ -221,6 +239,7 @@ protected function collectModuleHookImplementations($dir, $module, $module_preg) } } } } /** * Filter iterator callback. Allows include files and .php files in src/Hook. Loading core/modules/system/tests/modules/hook_collector_skip_procedural/hook_collector_skip_procedural.info.yml 0 → 100644 +5 −0 Original line number Diff line number Diff line name: 'Test skipping procedural for whole module' type: module description: 'Test skipping procedural for whole module.' package: Testing version: VERSION core/modules/system/tests/modules/hook_collector_skip_procedural/hook_collector_skip_procedural.module 0 → 100644 +18 −0 Original line number Diff line number Diff line <?php /** * @file * Implement hooks. */ declare(strict_types=1); /** * This implements a hook but should not be picked up. * * We have set procedural_hooks: skip. */ function hook_collector_skip_procedural_cache_flush(): void { // Set a global value we can check in test code. $GLOBALS['skip_procedural_all'] = 'skip_procedural_all'; } core/modules/system/tests/modules/hook_collector_skip_procedural/hook_collector_skip_procedural.services.yml 0 → 100644 +2 −0 Original line number Diff line number Diff line parameters: hook_collector_skip_procedural.hooks_converted: true Loading
core/lib/Drupal/Core/Hook/Attribute/StopProceduralHookScan.php 0 → 100644 +16 −0 Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Core\Hook\Attribute; /** * Defines a StopProceduralHookScan attribute object. * * This allows contrib and core to mark when a file has no more * procedural hooks. */ #[\Attribute(\Attribute::TARGET_FUNCTION)] class StopProceduralHookScan { }
core/lib/Drupal/Core/Hook/HookCollectorPass.php +71 −52 Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ use Drupal\Core\Extension\ProceduralCall; use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Hook\Attribute\LegacyHook; use Drupal\Core\Hook\Attribute\StopProceduralHookScan; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; Loading Loading @@ -74,7 +75,7 @@ class HookCollectorPass implements CompilerPassInterface { * {@inheritdoc} */ public function process(ContainerBuilder $container): void { $collector = static::collectAllHookImplementations($container->getParameter('container.modules')); $collector = static::collectAllHookImplementations($container->getParameter('container.modules'), $container); $map = []; $container->register(ProceduralCall::class, ProceduralCall::class) ->addArgument($collector->includes); Loading Loading @@ -123,6 +124,8 @@ public function process(ContainerBuilder $container): void { * @param array $module_filenames * An associative array. Keys are the module names, values are relevant * info yml file path. * @param Symfony\Component\DependencyInjection\ContainerBuilder|null $container * The container. * * @return static * A HookCollectorPass instance holding all hook implementations and Loading @@ -130,15 +133,18 @@ public function process(ContainerBuilder $container): void { * * @internal * This method is only used by ModuleHandler. * * * @todo Pass only $container when ModuleHandler->add is removed https://www.drupal.org/project/drupal/issues/3481778 */ public static function collectAllHookImplementations(array $module_filenames): static { public static function collectAllHookImplementations(array $module_filenames, ?ContainerBuilder $container = NULL): 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); $skip_procedural = isset($container) ? $container->hasParameter("$module.hooks_converted") : FALSE; $collector->collectModuleHookImplementations(dirname($info['pathname']), $module, $module_preg, $skip_procedural); } return $collector; } Loading @@ -153,13 +159,21 @@ public static function collectAllHookImplementations(array $module_filenames): s * @param $module_preg * A regular expression matching every module, longer module names are * matched first. * @param $skip_procedural * Skip the procedural check for the current module. * * @return void */ protected function collectModuleHookImplementations($dir, $module, $module_preg): void { protected function collectModuleHookImplementations($dir, $module, $module_preg, bool $skip_procedural): void { $hook_file_cache = FileCacheFactory::get('hook_implementations'); $procedural_hook_file_cache = FileCacheFactory::get('procedural_hook_implementations:' . $module_preg); // Check only hook classes. if ($skip_procedural) { $dir = $dir . '/src/Hook'; } if (is_dir($dir)) { $iterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::FOLLOW_SYMLINKS); $iterator = new \RecursiveCallbackFilterIterator($iterator, static::filterIterator(...)); $iterator = new \RecursiveIteratorIterator($iterator); Loading @@ -169,9 +183,10 @@ protected function collectModuleHookImplementations($dir, $module, $module_preg) $extension = $fileinfo->getExtension(); $filename = $fileinfo->getPathname(); if (($extension === 'module' || $extension === 'profile') && !$iterator->getDepth()) { if (($extension === 'module' || $extension === 'profile') && !$iterator->getDepth() && !$skip_procedural) { // There is an expectation for all modules and profiles to be loaded. // .module and .profile files are not supposed to be in subdirectories. include_once $filename; } if ($extension === 'php') { Loading @@ -196,13 +211,16 @@ protected function collectModuleHookImplementations($dir, $module, $module_preg) $this->addFromAttribute($attribute, $class, $module); } } else { elseif (!$skip_procedural) { $implementations = $procedural_hook_file_cache->get($filename); if ($implementations === NULL) { $finder = MockFileFinder::create($filename); $parser = new StaticReflectionParser('', $finder); $implementations = []; foreach ($parser->getMethodAttributes() as $function => $attributes) { if (StaticReflectionParser::hasAttribute($attributes, StopProceduralHookScan::class)) { break; } if (!StaticReflectionParser::hasAttribute($attributes, LegacyHook::class) && preg_match($module_preg, $function, $matches)) { $implementations[] = ['function' => $function, 'module' => $matches['module'], 'hook' => $matches['hook']]; } Loading @@ -221,6 +239,7 @@ protected function collectModuleHookImplementations($dir, $module, $module_preg) } } } } /** * Filter iterator callback. Allows include files and .php files in src/Hook. Loading
core/modules/system/tests/modules/hook_collector_skip_procedural/hook_collector_skip_procedural.info.yml 0 → 100644 +5 −0 Original line number Diff line number Diff line name: 'Test skipping procedural for whole module' type: module description: 'Test skipping procedural for whole module.' package: Testing version: VERSION
core/modules/system/tests/modules/hook_collector_skip_procedural/hook_collector_skip_procedural.module 0 → 100644 +18 −0 Original line number Diff line number Diff line <?php /** * @file * Implement hooks. */ declare(strict_types=1); /** * This implements a hook but should not be picked up. * * We have set procedural_hooks: skip. */ function hook_collector_skip_procedural_cache_flush(): void { // Set a global value we can check in test code. $GLOBALS['skip_procedural_all'] = 'skip_procedural_all'; }
core/modules/system/tests/modules/hook_collector_skip_procedural/hook_collector_skip_procedural.services.yml 0 → 100644 +2 −0 Original line number Diff line number Diff line parameters: hook_collector_skip_procedural.hooks_converted: true