Loading core/lib/Drupal/Core/Hook/HookCollectorBase.php 0 → 100644 +98 −0 Changes for core/lib/Drupal/Core/Hook/HookCollectorBase.php: 98 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Core\Hook; use Drupal\Core\Hook\Attribute\HookAttributeInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; /** * Provides shared hook collection functionality. * * @internal */ abstract class HookCollectorBase { /** * Gets the files that can contain hook implementations. * * @param string $dir * The extension directory. * @param list<string> $procedural_files * The root-level procedural hook implementation files. * * @return \Generator<\SplFileInfo> * The procedural hook files and PHP files in src/Hook. */ protected function getHookFileIterator(string $dir, array $procedural_files): \Generator { foreach ($procedural_files as $filename) { $filename = "$dir/$filename"; if (is_file($filename)) { yield new \SplFileInfo($filename); } } $hook_directory = "$dir/src/Hook"; if (!is_dir($hook_directory)) { return; } $flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::FOLLOW_SYMLINKS; $iterator = new \RecursiveDirectoryIterator($hook_directory, $flags); $iterator = new \RecursiveCallbackFilterIterator($iterator, static fn (\SplFileInfo $fileinfo): bool => $fileinfo->isDir() || $fileinfo->getExtension() === 'php'); foreach (new \RecursiveIteratorIterator($iterator) as $fileinfo) { yield $fileinfo; } } /** * Registers hook implementation services. * * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container * The container builder. * @param array<string, array<string, string>> $implementations_by_hook * Implementations keyed by hook name and identifier. */ protected static function registerHookServices(ContainerBuilder $container, array $implementations_by_hook): void { $classes_map = []; foreach ($implementations_by_hook as $hook_implementations) { foreach (array_keys($hook_implementations) as $identifier) { $parts = explode('::', $identifier, 2); if (isset($parts[1])) { $classes_map[$parts[0]] = TRUE; } } } foreach (array_keys($classes_map) as $class) { if (!$container->hasDefinition($class)) { $container ->register($class, $class) ->setAutowired(TRUE); } } } /** * Gets attribute instances from a class reflection. * * @param \ReflectionClass $reflection_class * The class reflection. * * @return array<string, list<\Drupal\Core\Hook\Attribute\HookAttributeInterface>> * Attribute instances keyed by method name. */ protected static function getAttributeInstances(\ReflectionClass $reflection_class): array { $attributes = []; $reflections = $reflection_class->getMethods(\ReflectionMethod::IS_PUBLIC); $reflections[] = $reflection_class; foreach ($reflections as $reflection) { if ($reflection_attributes = $reflection->getAttributes(HookAttributeInterface::class, \ReflectionAttribute::IS_INSTANCEOF)) { $method = $reflection instanceof \ReflectionMethod ? $reflection->getName() : '__invoke'; $attributes[$method] = array_map(static fn (\ReflectionAttribute $reflection_attribute) => $reflection_attribute->newInstance(), $reflection_attributes); } } return $attributes; } } core/lib/Drupal/Core/Hook/HookCollectorPass.php +11 −76 Changes for core/lib/Drupal/Core/Hook/HookCollectorPass.php: 11 added lines, 76 removed lines. Original line number Diff line number Diff line Loading @@ -41,7 +41,14 @@ * * @internal */ class HookCollectorPass implements CompilerPassInterface { class HookCollectorPass extends HookCollectorBase implements CompilerPassInterface { /** * The extensions of procedural hook implementation files. * * @var string[] */ protected array $proceduralFileExtensions = ['module', 'profile', 'install']; /** * OOP implementation module names keyed by hook name and "$class::$method". Loading Loading @@ -255,38 +262,6 @@ protected static function applyOrderOperations(array &$implementation_list, arra $implementation_list = array_replace($identifiers, $module_finder); } /** * Registers the hook implementation services. * * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container * The container builder. * @param array<string, array<string, string>> $implementationsByHook * Implementations, as module names keyed by hook name and "$class::$method" * or $function identifier. */ protected static function registerHookServices( ContainerBuilder $container, array $implementationsByHook, ): void { $classesMap = []; foreach ($implementationsByHook as $hookImplementations) { foreach (array_keys($hookImplementations) as $identifier) { $parts = explode('::', $identifier, 2); if (isset($parts[1])) { $classesMap[$parts[0]] = TRUE; } } } foreach (array_keys($classesMap) as $class) { if (!$container->hasDefinition($class)) { $container ->register($class, $class) ->setAutowired(TRUE); } } } /** * Collects all hook implementations. * Loading Loading @@ -360,11 +335,8 @@ protected function collectModuleHookImplementations($dir, $module, $current_modu // Hash to prevent massive key sizes. $procedural_hook_file_cache = FileCacheFactory::get('procedural_hook_implementations:' . hash('xxh3', $all_modules_preg)); $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) { $procedural_files = array_map(fn (string $extension): string => "$module.$extension", $this->proceduralFileExtensions); foreach ($this->getHookFileIterator($dir, $procedural_files) as $fileinfo) { assert($fileinfo instanceof \SplFileInfo); $extension = $fileinfo->getExtension(); $filename = $fileinfo->getPathname(); Loading @@ -383,7 +355,7 @@ protected function collectModuleHookImplementations($dir, $module, $current_modu // forcibly invalidating the opcode cache. // @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.revalidate-freq OpCodeCache::invalidate($filename); $namespace = preg_replace('#^src/#', "Drupal/$module/", $iterator->getSubPath()); $namespace = preg_replace('#^src/#', "Drupal/$module/", substr($fileinfo->getPath(), strlen($dir) + 1)); $class = $namespace . '/' . $fileinfo->getBasename('.php'); $class = str_replace('/', '\\', $class); $class_attributes = []; Loading Loading @@ -519,21 +491,6 @@ public static function hasAnyAttribute(array $existingAttributes, array $attribu return FALSE; } /** * 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()) { return $sub_path_name === 'src' || $sub_path_name === 'src/Hook'; } return in_array($extension, ['module', 'profile', 'install']); } /** * Adds a procedural hook implementation. * Loading Loading @@ -579,26 +536,4 @@ public static function checkForProceduralOnlyHooks(Hook $hookAttribute, string $ } } /** * Get attribute instances from class and method reflections. * * @param \ReflectionClass $reflectionClass * A reflected class. * * @return array<string, list<\Drupal\Core\Hook\Attribute\HookAttributeInterface>> * Lists of Hook attribute instances by method name. */ protected static function getAttributeInstances(\ReflectionClass $reflectionClass): array { $attributes = []; $reflections = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); $reflections[] = $reflectionClass; foreach ($reflections as $reflection) { if ($reflectionAttributes = $reflection->getAttributes(HookAttributeInterface::class, \ReflectionAttribute::IS_INSTANCEOF)) { $method = $reflection instanceof \ReflectionMethod ? $reflection->getName() : '__invoke'; $attributes[$method] = array_map(static fn (\ReflectionAttribute $ra) => $ra->newInstance(), $reflectionAttributes); } } return $attributes; } } core/lib/Drupal/Core/Hook/ThemeHookCollectorPass.php +3 −80 Changes for core/lib/Drupal/Core/Hook/ThemeHookCollectorPass.php: 3 added lines, 80 removed lines. Original line number Diff line number Diff line Loading @@ -9,7 +9,6 @@ use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Component\Utility\OpCodeCache; use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Hook\Attribute\HookAttributeInterface; use Drupal\Core\Hook\Attribute\LegacyHook; use Drupal\Core\Hook\Attribute\RemoveHook; use Drupal\Core\Hook\Attribute\ProceduralHookScanStop; Loading @@ -35,7 +34,7 @@ * * @internal */ class ThemeHookCollectorPass implements CompilerPassInterface { class ThemeHookCollectorPass extends HookCollectorBase implements CompilerPassInterface { /** * OOP implementation theme names keyed by hook name and "$class::$method". Loading Loading @@ -227,11 +226,7 @@ protected function collectThemeHookImplementations($dir, $theme, $currentThemePr $hookFileCache = FileCacheFactory::get('theme_hook_implementations'); $proceduralHookFileCache = FileCacheFactory::get('theme_procedural_hook_implementations:' . $allThemesPreg); $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) { foreach ($this->getHookFileIterator($dir, ["$theme.theme", 'theme-settings.php']) as $fileinfo) { assert($fileinfo instanceof \SplFileInfo); $fileExtension = $fileinfo->getExtension(); $filename = $fileinfo->getPathname(); Loading Loading @@ -259,7 +254,7 @@ protected function collectThemeHookImplementations($dir, $theme, $currentThemePr // forcibly invalidating the opcode cache. // @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.revalidate-freq OpCodeCache::invalidate($filename); $namespace = preg_replace('#^src/#', "Drupal/$theme/", $iterator->getSubPath()); $namespace = preg_replace('#^src/#', "Drupal/$theme/", substr($fileinfo->getPath(), strlen($dir) + 1)); $class = $namespace . '/' . $fileinfo->getBasename('.php'); $class = str_replace('/', '\\', $class); $attributes = []; Loading Loading @@ -342,56 +337,6 @@ protected function getFilteredImplementations(): array { return $implementationsByHook; } /** * Registers the hook implementation services. * * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container * The container builder. * @param array<string, array<string, string>> $implementationsByHook * Implementations, as module names keyed by hook name and "$class::$method" * or $function identifier. */ protected static function registerHookServices( ContainerBuilder $container, array $implementationsByHook, ): void { $classesMap = []; foreach ($implementationsByHook as $hookImplementations) { foreach (array_keys($hookImplementations) as $identifier) { $parts = explode('::', $identifier, 2); if (isset($parts[1])) { $classesMap[$parts[0]] = TRUE; } } } foreach (array_keys($classesMap) as $class) { if (!$container->hasDefinition($class)) { $container ->register($class, $class) ->setAutowired(TRUE); } } } /** * Filter iterator callback. Allows include files and .php files in src/Hook. */ protected static function filterIterator(\SplFileInfo $fileInfo, $key, \RecursiveDirectoryIterator $iterator): bool { $subPathName = $iterator->getSubPathname(); $extension = $fileInfo->getExtension(); if (str_starts_with($subPathName, 'src/Hook/')) { return $iterator->isDir() || $extension === 'php'; } if ($iterator->isDir()) { return $subPathName === 'src' || $subPathName === 'src/Hook'; } if ($fileInfo->getFilename() === 'theme-settings.php') { return TRUE; } return in_array($extension, ['theme']); } /** * Checks for hooks which can't be supported in theme classes. * Loading @@ -411,26 +356,4 @@ public static function checkInvalidHookParametersInThemes(Hook $hookAttribute, s } } /** * Get attribute instances from class and method reflections. * * @param \ReflectionClass $reflectionClass * A reflected class. * * @return array<string, list<\Drupal\Core\Hook\Attribute\HookAttributeInterface>> * Lists of Hook attribute instances by method name. */ protected static function getAttributeInstances(\ReflectionClass $reflectionClass): array { $attributes = []; $reflections = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); $reflections[] = $reflectionClass; foreach ($reflections as $reflection) { if ($reflectionAttributes = $reflection->getAttributes(HookAttributeInterface::class, \ReflectionAttribute::IS_INSTANCEOF)) { $method = $reflection instanceof \ReflectionMethod ? $reflection->getName() : '__invoke'; $attributes[$method] = array_map(static fn(\ReflectionAttribute $ra) => $ra->newInstance(), $reflectionAttributes); } } return $attributes; } } core/tests/Drupal/Tests/Core/Hook/HookCollectorPassTest.php +48 −1 Changes for core/tests/Drupal/Tests/Core/Hook/HookCollectorPassTest.php: 48 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Extension\ProceduralCall; use Drupal\Core\Hook\HookCollectorBase; use Drupal\Core\Hook\HookCollectorPass; use Drupal\Tests\UnitTestCase; use org\bovigo\vfs\vfsStream; Loading @@ -24,7 +25,7 @@ class HookCollectorPassTest extends UnitTestCase { * Tests collect all hook implementations. * * @legacy-covers ::collectAllHookImplementations * @legacy-covers ::filterIterator * @legacy-covers ::getHookFileIterator */ public function testCollectAllHookImplementations(): void { vfsStream::setup('drupal_root'); Loading Loading @@ -67,6 +68,52 @@ function test_module_should_be_skipped(); ); } public function testCollectsOnlyRootProceduralAndHookDirectoryFiles(): void { vfsStream::setup('drupal_root'); vfsStream::create([ 'test_module' => [ 'test_module.module' => '', 'test_module.install' => '', 'test_module.profile' => '', 'another_module.module' => '', 'ignored.php' => '', 'src' => [ 'Hook' => [ 'TestHooks.php' => '', 'Subdirectory' => ['NestedHooks.php' => ''], ], 'Other' => ['IgnoredHooks.php' => ''], ], 'ignored_directory' => ['ignored.module' => ''], ], ]); $collector = new HookCollectorPass(); $get_files = \Closure::bind( static function (HookCollectorPass $collector, string $directory, string $module): array { $procedural_files = [ "$module.module", "$module.profile", "$module.install", ]; return iterator_to_array($collector->getHookFileIterator($directory, $procedural_files)); }, NULL, HookCollectorBase::class, ); $files = $get_files($collector, 'vfs://drupal_root/test_module', 'test_module'); $paths = array_map(static fn (\SplFileInfo $file): string => $file->getPathname(), $files); sort($paths); $this->assertSame([ 'vfs://drupal_root/test_module/src/Hook/Subdirectory/NestedHooks.php', 'vfs://drupal_root/test_module/src/Hook/TestHooks.php', 'vfs://drupal_root/test_module/test_module.install', 'vfs://drupal_root/test_module/test_module.module', 'vfs://drupal_root/test_module/test_module.profile', ], $paths); } /** * Tests prefix ownership of procedural hooks. * Loading Loading
core/lib/Drupal/Core/Hook/HookCollectorBase.php 0 → 100644 +98 −0 Changes for core/lib/Drupal/Core/Hook/HookCollectorBase.php: 98 added lines, 0 removed lines. Original line number Diff line number Diff line <?php declare(strict_types=1); namespace Drupal\Core\Hook; use Drupal\Core\Hook\Attribute\HookAttributeInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; /** * Provides shared hook collection functionality. * * @internal */ abstract class HookCollectorBase { /** * Gets the files that can contain hook implementations. * * @param string $dir * The extension directory. * @param list<string> $procedural_files * The root-level procedural hook implementation files. * * @return \Generator<\SplFileInfo> * The procedural hook files and PHP files in src/Hook. */ protected function getHookFileIterator(string $dir, array $procedural_files): \Generator { foreach ($procedural_files as $filename) { $filename = "$dir/$filename"; if (is_file($filename)) { yield new \SplFileInfo($filename); } } $hook_directory = "$dir/src/Hook"; if (!is_dir($hook_directory)) { return; } $flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::FOLLOW_SYMLINKS; $iterator = new \RecursiveDirectoryIterator($hook_directory, $flags); $iterator = new \RecursiveCallbackFilterIterator($iterator, static fn (\SplFileInfo $fileinfo): bool => $fileinfo->isDir() || $fileinfo->getExtension() === 'php'); foreach (new \RecursiveIteratorIterator($iterator) as $fileinfo) { yield $fileinfo; } } /** * Registers hook implementation services. * * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container * The container builder. * @param array<string, array<string, string>> $implementations_by_hook * Implementations keyed by hook name and identifier. */ protected static function registerHookServices(ContainerBuilder $container, array $implementations_by_hook): void { $classes_map = []; foreach ($implementations_by_hook as $hook_implementations) { foreach (array_keys($hook_implementations) as $identifier) { $parts = explode('::', $identifier, 2); if (isset($parts[1])) { $classes_map[$parts[0]] = TRUE; } } } foreach (array_keys($classes_map) as $class) { if (!$container->hasDefinition($class)) { $container ->register($class, $class) ->setAutowired(TRUE); } } } /** * Gets attribute instances from a class reflection. * * @param \ReflectionClass $reflection_class * The class reflection. * * @return array<string, list<\Drupal\Core\Hook\Attribute\HookAttributeInterface>> * Attribute instances keyed by method name. */ protected static function getAttributeInstances(\ReflectionClass $reflection_class): array { $attributes = []; $reflections = $reflection_class->getMethods(\ReflectionMethod::IS_PUBLIC); $reflections[] = $reflection_class; foreach ($reflections as $reflection) { if ($reflection_attributes = $reflection->getAttributes(HookAttributeInterface::class, \ReflectionAttribute::IS_INSTANCEOF)) { $method = $reflection instanceof \ReflectionMethod ? $reflection->getName() : '__invoke'; $attributes[$method] = array_map(static fn (\ReflectionAttribute $reflection_attribute) => $reflection_attribute->newInstance(), $reflection_attributes); } } return $attributes; } }
core/lib/Drupal/Core/Hook/HookCollectorPass.php +11 −76 Changes for core/lib/Drupal/Core/Hook/HookCollectorPass.php: 11 added lines, 76 removed lines. Original line number Diff line number Diff line Loading @@ -41,7 +41,14 @@ * * @internal */ class HookCollectorPass implements CompilerPassInterface { class HookCollectorPass extends HookCollectorBase implements CompilerPassInterface { /** * The extensions of procedural hook implementation files. * * @var string[] */ protected array $proceduralFileExtensions = ['module', 'profile', 'install']; /** * OOP implementation module names keyed by hook name and "$class::$method". Loading Loading @@ -255,38 +262,6 @@ protected static function applyOrderOperations(array &$implementation_list, arra $implementation_list = array_replace($identifiers, $module_finder); } /** * Registers the hook implementation services. * * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container * The container builder. * @param array<string, array<string, string>> $implementationsByHook * Implementations, as module names keyed by hook name and "$class::$method" * or $function identifier. */ protected static function registerHookServices( ContainerBuilder $container, array $implementationsByHook, ): void { $classesMap = []; foreach ($implementationsByHook as $hookImplementations) { foreach (array_keys($hookImplementations) as $identifier) { $parts = explode('::', $identifier, 2); if (isset($parts[1])) { $classesMap[$parts[0]] = TRUE; } } } foreach (array_keys($classesMap) as $class) { if (!$container->hasDefinition($class)) { $container ->register($class, $class) ->setAutowired(TRUE); } } } /** * Collects all hook implementations. * Loading Loading @@ -360,11 +335,8 @@ protected function collectModuleHookImplementations($dir, $module, $current_modu // Hash to prevent massive key sizes. $procedural_hook_file_cache = FileCacheFactory::get('procedural_hook_implementations:' . hash('xxh3', $all_modules_preg)); $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) { $procedural_files = array_map(fn (string $extension): string => "$module.$extension", $this->proceduralFileExtensions); foreach ($this->getHookFileIterator($dir, $procedural_files) as $fileinfo) { assert($fileinfo instanceof \SplFileInfo); $extension = $fileinfo->getExtension(); $filename = $fileinfo->getPathname(); Loading @@ -383,7 +355,7 @@ protected function collectModuleHookImplementations($dir, $module, $current_modu // forcibly invalidating the opcode cache. // @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.revalidate-freq OpCodeCache::invalidate($filename); $namespace = preg_replace('#^src/#', "Drupal/$module/", $iterator->getSubPath()); $namespace = preg_replace('#^src/#', "Drupal/$module/", substr($fileinfo->getPath(), strlen($dir) + 1)); $class = $namespace . '/' . $fileinfo->getBasename('.php'); $class = str_replace('/', '\\', $class); $class_attributes = []; Loading Loading @@ -519,21 +491,6 @@ public static function hasAnyAttribute(array $existingAttributes, array $attribu return FALSE; } /** * 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()) { return $sub_path_name === 'src' || $sub_path_name === 'src/Hook'; } return in_array($extension, ['module', 'profile', 'install']); } /** * Adds a procedural hook implementation. * Loading Loading @@ -579,26 +536,4 @@ public static function checkForProceduralOnlyHooks(Hook $hookAttribute, string $ } } /** * Get attribute instances from class and method reflections. * * @param \ReflectionClass $reflectionClass * A reflected class. * * @return array<string, list<\Drupal\Core\Hook\Attribute\HookAttributeInterface>> * Lists of Hook attribute instances by method name. */ protected static function getAttributeInstances(\ReflectionClass $reflectionClass): array { $attributes = []; $reflections = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); $reflections[] = $reflectionClass; foreach ($reflections as $reflection) { if ($reflectionAttributes = $reflection->getAttributes(HookAttributeInterface::class, \ReflectionAttribute::IS_INSTANCEOF)) { $method = $reflection instanceof \ReflectionMethod ? $reflection->getName() : '__invoke'; $attributes[$method] = array_map(static fn (\ReflectionAttribute $ra) => $ra->newInstance(), $reflectionAttributes); } } return $attributes; } }
core/lib/Drupal/Core/Hook/ThemeHookCollectorPass.php +3 −80 Changes for core/lib/Drupal/Core/Hook/ThemeHookCollectorPass.php: 3 added lines, 80 removed lines. Original line number Diff line number Diff line Loading @@ -9,7 +9,6 @@ use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Component\Utility\OpCodeCache; use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Hook\Attribute\HookAttributeInterface; use Drupal\Core\Hook\Attribute\LegacyHook; use Drupal\Core\Hook\Attribute\RemoveHook; use Drupal\Core\Hook\Attribute\ProceduralHookScanStop; Loading @@ -35,7 +34,7 @@ * * @internal */ class ThemeHookCollectorPass implements CompilerPassInterface { class ThemeHookCollectorPass extends HookCollectorBase implements CompilerPassInterface { /** * OOP implementation theme names keyed by hook name and "$class::$method". Loading Loading @@ -227,11 +226,7 @@ protected function collectThemeHookImplementations($dir, $theme, $currentThemePr $hookFileCache = FileCacheFactory::get('theme_hook_implementations'); $proceduralHookFileCache = FileCacheFactory::get('theme_procedural_hook_implementations:' . $allThemesPreg); $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) { foreach ($this->getHookFileIterator($dir, ["$theme.theme", 'theme-settings.php']) as $fileinfo) { assert($fileinfo instanceof \SplFileInfo); $fileExtension = $fileinfo->getExtension(); $filename = $fileinfo->getPathname(); Loading Loading @@ -259,7 +254,7 @@ protected function collectThemeHookImplementations($dir, $theme, $currentThemePr // forcibly invalidating the opcode cache. // @see https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.revalidate-freq OpCodeCache::invalidate($filename); $namespace = preg_replace('#^src/#', "Drupal/$theme/", $iterator->getSubPath()); $namespace = preg_replace('#^src/#', "Drupal/$theme/", substr($fileinfo->getPath(), strlen($dir) + 1)); $class = $namespace . '/' . $fileinfo->getBasename('.php'); $class = str_replace('/', '\\', $class); $attributes = []; Loading Loading @@ -342,56 +337,6 @@ protected function getFilteredImplementations(): array { return $implementationsByHook; } /** * Registers the hook implementation services. * * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container * The container builder. * @param array<string, array<string, string>> $implementationsByHook * Implementations, as module names keyed by hook name and "$class::$method" * or $function identifier. */ protected static function registerHookServices( ContainerBuilder $container, array $implementationsByHook, ): void { $classesMap = []; foreach ($implementationsByHook as $hookImplementations) { foreach (array_keys($hookImplementations) as $identifier) { $parts = explode('::', $identifier, 2); if (isset($parts[1])) { $classesMap[$parts[0]] = TRUE; } } } foreach (array_keys($classesMap) as $class) { if (!$container->hasDefinition($class)) { $container ->register($class, $class) ->setAutowired(TRUE); } } } /** * Filter iterator callback. Allows include files and .php files in src/Hook. */ protected static function filterIterator(\SplFileInfo $fileInfo, $key, \RecursiveDirectoryIterator $iterator): bool { $subPathName = $iterator->getSubPathname(); $extension = $fileInfo->getExtension(); if (str_starts_with($subPathName, 'src/Hook/')) { return $iterator->isDir() || $extension === 'php'; } if ($iterator->isDir()) { return $subPathName === 'src' || $subPathName === 'src/Hook'; } if ($fileInfo->getFilename() === 'theme-settings.php') { return TRUE; } return in_array($extension, ['theme']); } /** * Checks for hooks which can't be supported in theme classes. * Loading @@ -411,26 +356,4 @@ public static function checkInvalidHookParametersInThemes(Hook $hookAttribute, s } } /** * Get attribute instances from class and method reflections. * * @param \ReflectionClass $reflectionClass * A reflected class. * * @return array<string, list<\Drupal\Core\Hook\Attribute\HookAttributeInterface>> * Lists of Hook attribute instances by method name. */ protected static function getAttributeInstances(\ReflectionClass $reflectionClass): array { $attributes = []; $reflections = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); $reflections[] = $reflectionClass; foreach ($reflections as $reflection) { if ($reflectionAttributes = $reflection->getAttributes(HookAttributeInterface::class, \ReflectionAttribute::IS_INSTANCEOF)) { $method = $reflection instanceof \ReflectionMethod ? $reflection->getName() : '__invoke'; $attributes[$method] = array_map(static fn(\ReflectionAttribute $ra) => $ra->newInstance(), $reflectionAttributes); } } return $attributes; } }
core/tests/Drupal/Tests/Core/Hook/HookCollectorPassTest.php +48 −1 Changes for core/tests/Drupal/Tests/Core/Hook/HookCollectorPassTest.php: 48 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Extension\ProceduralCall; use Drupal\Core\Hook\HookCollectorBase; use Drupal\Core\Hook\HookCollectorPass; use Drupal\Tests\UnitTestCase; use org\bovigo\vfs\vfsStream; Loading @@ -24,7 +25,7 @@ class HookCollectorPassTest extends UnitTestCase { * Tests collect all hook implementations. * * @legacy-covers ::collectAllHookImplementations * @legacy-covers ::filterIterator * @legacy-covers ::getHookFileIterator */ public function testCollectAllHookImplementations(): void { vfsStream::setup('drupal_root'); Loading Loading @@ -67,6 +68,52 @@ function test_module_should_be_skipped(); ); } public function testCollectsOnlyRootProceduralAndHookDirectoryFiles(): void { vfsStream::setup('drupal_root'); vfsStream::create([ 'test_module' => [ 'test_module.module' => '', 'test_module.install' => '', 'test_module.profile' => '', 'another_module.module' => '', 'ignored.php' => '', 'src' => [ 'Hook' => [ 'TestHooks.php' => '', 'Subdirectory' => ['NestedHooks.php' => ''], ], 'Other' => ['IgnoredHooks.php' => ''], ], 'ignored_directory' => ['ignored.module' => ''], ], ]); $collector = new HookCollectorPass(); $get_files = \Closure::bind( static function (HookCollectorPass $collector, string $directory, string $module): array { $procedural_files = [ "$module.module", "$module.profile", "$module.install", ]; return iterator_to_array($collector->getHookFileIterator($directory, $procedural_files)); }, NULL, HookCollectorBase::class, ); $files = $get_files($collector, 'vfs://drupal_root/test_module', 'test_module'); $paths = array_map(static fn (\SplFileInfo $file): string => $file->getPathname(), $files); sort($paths); $this->assertSame([ 'vfs://drupal_root/test_module/src/Hook/Subdirectory/NestedHooks.php', 'vfs://drupal_root/test_module/src/Hook/TestHooks.php', 'vfs://drupal_root/test_module/test_module.install', 'vfs://drupal_root/test_module/test_module.module', 'vfs://drupal_root/test_module/test_module.profile', ], $paths); } /** * Tests prefix ownership of procedural hooks. * Loading