diff --git a/core/lib/Drupal/Core/Hook/HookCollectorPass.php b/core/lib/Drupal/Core/Hook/HookCollectorPass.php index 3d96ca99486fd355bfe131cbe7244323ed8823e1..7b1a816984b29fb9159bfc9e4b5603c72e1b2cb8 100644 --- a/core/lib/Drupal/Core/Hook/HookCollectorPass.php +++ b/core/lib/Drupal/Core/Hook/HookCollectorPass.php @@ -6,6 +6,7 @@ use Drupal\Component\Annotation\Doctrine\StaticReflectionParser; use Drupal\Component\Annotation\Reflection\MockFileFinder; +use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Core\Extension\ProceduralCall; use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Hook\Attribute\LegacyHook; @@ -156,6 +157,9 @@ public static function collectAllHookImplementations(array $module_filenames): s * @return void */ protected function collectModuleHookImplementations($dir, $module, $module_preg): void { + $hook_file_cache = FileCacheFactory::get('hook_implementations'); + $procedural_hook_file_cache = FileCacheFactory::get('procedural_hook_implementations:' . $module_preg); + $iterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::FOLLOW_SYMLINKS); $iterator = new \RecursiveCallbackFilterIterator($iterator, static::filterIterator(...)); $iterator = new \RecursiveIteratorIterator($iterator); @@ -163,32 +167,51 @@ protected function collectModuleHookImplementations($dir, $module, $module_preg) foreach ($iterator as $fileinfo) { assert($fileinfo instanceof \SplFileInfo); $extension = $fileinfo->getExtension(); + $filename = $fileinfo->getPathname(); + 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(); + include_once $filename; } 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) { + $cached = $hook_file_cache->get($filename); + if ($cached) { + $class = $cached['class']; + $attributes = $cached['attributes']; + } + else { + $namespace = preg_replace('#^src/#', "Drupal/$module/", $iterator->getSubPath()); + $class = $namespace . '/' . $fileinfo->getBasename('.php'); + $class = str_replace('/', '\\', $class); + $attributes = static::getHookAttributesInClass($class); + $hook_file_cache->set($filename, ['class' => $class, 'attributes' => $attributes]); + } + foreach ($attributes 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']); + $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, LegacyHook::class) && preg_match($module_preg, $function, $matches)) { + $implementations[] = ['function' => $function, 'module' => $matches['module'], 'hook' => $matches['hook']]; + } } + $procedural_hook_file_cache->set($filename, $implementations); + } + foreach ($implementations as $implementation) { + $this->addProceduralImplementation($fileinfo, $implementation['hook'], $implementation['module'], $implementation['function']); } } if ($extension === 'inc') { $parts = explode('.', $fileinfo->getFilename()); if (count($parts) === 3 && $parts[0] === $module) { - $this->groupIncludes[$parts[1]][] = $fileinfo->getPathname(); + $this->groupIncludes[$parts[1]][] = $filename; } } } diff --git a/core/modules/system/tests/modules/hook_collector_on_behalf/hook_collector_on_behalf.info.yml b/core/modules/system/tests/modules/hook_collector_on_behalf/hook_collector_on_behalf.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..872192353948b629f3427116fa15073c7537fe85 --- /dev/null +++ b/core/modules/system/tests/modules/hook_collector_on_behalf/hook_collector_on_behalf.info.yml @@ -0,0 +1,5 @@ +name: 'Test Hooks on behalf of other modules' +type: module +description: 'Test hooks invoked on behalf of other modules when installed later.' +package: Testing +version: VERSION diff --git a/core/modules/system/tests/modules/hook_collector_on_behalf/src/Hook/OnBehalfOfOtherModuleHook.php b/core/modules/system/tests/modules/hook_collector_on_behalf/src/Hook/OnBehalfOfOtherModuleHook.php new file mode 100644 index 0000000000000000000000000000000000000000..5af29ad02f5249c48cf396889651617a848592ba --- /dev/null +++ b/core/modules/system/tests/modules/hook_collector_on_behalf/src/Hook/OnBehalfOfOtherModuleHook.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_collector_on_behalf\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementation on behalf of another module. + */ +class OnBehalfOfOtherModuleHook { + + /** + * Implements hook_module_preinstall(). + */ + #[Hook('cache_flush', module: 'respond_install_uninstall_hook_test')] + public function flush(): void { + // Set a global value we can check in test code. + $GLOBALS['on_behalf_oop'] = 'on_behalf_oop'; + } + +} diff --git a/core/modules/system/tests/modules/hook_collector_on_behalf_procedural/hook_collector_on_behalf_procedural.info.yml b/core/modules/system/tests/modules/hook_collector_on_behalf_procedural/hook_collector_on_behalf_procedural.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..872192353948b629f3427116fa15073c7537fe85 --- /dev/null +++ b/core/modules/system/tests/modules/hook_collector_on_behalf_procedural/hook_collector_on_behalf_procedural.info.yml @@ -0,0 +1,5 @@ +name: 'Test Hooks on behalf of other modules' +type: module +description: 'Test hooks invoked on behalf of other modules when installed later.' +package: Testing +version: VERSION diff --git a/core/modules/system/tests/modules/hook_collector_on_behalf_procedural/hook_collector_on_behalf_procedural.module b/core/modules/system/tests/modules/hook_collector_on_behalf_procedural/hook_collector_on_behalf_procedural.module new file mode 100644 index 0000000000000000000000000000000000000000..0fd5bb9d78d19637529bc3300c56d4bc01d03340 --- /dev/null +++ b/core/modules/system/tests/modules/hook_collector_on_behalf_procedural/hook_collector_on_behalf_procedural.module @@ -0,0 +1,18 @@ +<?php + +/** + * @file + * Implement hooks. + */ + +declare(strict_types=1); + +/** + * This implements a hook on behalf of another module. + * + * We do not have implements so this does not get converted. + */ +function respond_install_uninstall_hook_test_cache_flush(): void { + // Set a global value we can check in test code. + $GLOBALS['on_behalf_procedural'] = 'on_behalf_procedural'; +} diff --git a/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php b/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php index c7e21c270219e4029753b3316b499135d8a61bff..e05ca8ab6019408034c259179acbcfd372a5fc15 100644 --- a/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php +++ b/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php @@ -82,4 +82,23 @@ public function testOrdering(): void { $this->assertLessThan($priorities['drupal_hook.order2']['order'], $priorities['drupal_hook.order2']['module_handler_test_all2_order2']); } + /** + * Test hooks implemented on behalf of an uninstalled module. + * + * They should be picked up but only executed when the other + * module is installed. + */ + public function testHooksImplementedOnBehalfFileCache(): void { + $module_installer = $this->container->get('module_installer'); + $this->assertTrue($module_installer->install(['hook_collector_on_behalf'])); + $this->assertTrue($module_installer->install(['hook_collector_on_behalf_procedural'])); + drupal_flush_all_caches(); + $this->assertFalse(isset($GLOBALS['on_behalf_oop'])); + $this->assertFalse(isset($GLOBALS['on_behalf_procedural'])); + $this->assertTrue($module_installer->install(['respond_install_uninstall_hook_test'])); + drupal_flush_all_caches(); + $this->assertTrue(isset($GLOBALS['on_behalf_oop'])); + $this->assertTrue(isset($GLOBALS['on_behalf_procedural'])); + } + }