Skip to content
Snippets Groups Projects
Commit 756c93fb authored by catch's avatar catch
Browse files

Issue #3490355 by nicxvan, godotislate, catch: Add procedural hook short circuit per module or file

parent fa75872d
No related branches found
No related tags found
No related merge requests found
Showing with 182 additions and 52 deletions
<?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 {
}
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
use Drupal\Core\Extension\ProceduralCall; use Drupal\Core\Extension\ProceduralCall;
use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Hook\Attribute\LegacyHook; use Drupal\Core\Hook\Attribute\LegacyHook;
use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
...@@ -74,7 +75,7 @@ class HookCollectorPass implements CompilerPassInterface { ...@@ -74,7 +75,7 @@ class HookCollectorPass implements CompilerPassInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function process(ContainerBuilder $container): void { public function process(ContainerBuilder $container): void {
$collector = static::collectAllHookImplementations($container->getParameter('container.modules')); $collector = static::collectAllHookImplementations($container->getParameter('container.modules'), $container);
$map = []; $map = [];
$container->register(ProceduralCall::class, ProceduralCall::class) $container->register(ProceduralCall::class, ProceduralCall::class)
->addArgument($collector->includes); ->addArgument($collector->includes);
...@@ -123,6 +124,8 @@ public function process(ContainerBuilder $container): void { ...@@ -123,6 +124,8 @@ public function process(ContainerBuilder $container): void {
* @param array $module_filenames * @param array $module_filenames
* An associative array. Keys are the module names, values are relevant * An associative array. Keys are the module names, values are relevant
* info yml file path. * info yml file path.
* @param Symfony\Component\DependencyInjection\ContainerBuilder|null $container
* The container.
* *
* @return static * @return static
* A HookCollectorPass instance holding all hook implementations and * A HookCollectorPass instance holding all hook implementations and
...@@ -130,15 +133,18 @@ public function process(ContainerBuilder $container): void { ...@@ -130,15 +133,18 @@ public function process(ContainerBuilder $container): void {
* *
* @internal * @internal
* This method is only used by ModuleHandler. * 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)); $modules = array_map(fn ($x) => preg_quote($x, '/'), array_keys($module_filenames));
// Longer modules first. // Longer modules first.
usort($modules, fn($a, $b) => strlen($b) - strlen($a)); 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]+$))/'; $module_preg = '/^(?<function>(?<module>' . implode('|', $modules) . ')_(?!preprocess_)(?!update_\d)(?<hook>[a-zA-Z0-9_\x80-\xff]+$))/';
$collector = new static(); $collector = new static();
foreach ($module_filenames as $module => $info) { 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; return $collector;
} }
...@@ -153,70 +159,83 @@ public static function collectAllHookImplementations(array $module_filenames): s ...@@ -153,70 +159,83 @@ public static function collectAllHookImplementations(array $module_filenames): s
* @param $module_preg * @param $module_preg
* A regular expression matching every module, longer module names are * A regular expression matching every module, longer module names are
* matched first. * matched first.
* @param $skip_procedural
* Skip the procedural check for the current module.
* *
* @return void * @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'); $hook_file_cache = FileCacheFactory::get('hook_implementations');
$procedural_hook_file_cache = FileCacheFactory::get('procedural_hook_implementations:' . $module_preg); $procedural_hook_file_cache = FileCacheFactory::get('procedural_hook_implementations:' . $module_preg);
$iterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::FOLLOW_SYMLINKS); // Check only hook classes.
$iterator = new \RecursiveCallbackFilterIterator($iterator, static::filterIterator(...)); if ($skip_procedural) {
$iterator = new \RecursiveIteratorIterator($iterator); $dir = $dir . '/src/Hook';
/** @var \RecursiveDirectoryIterator | \RecursiveIteratorIterator $iterator*/ }
foreach ($iterator as $fileinfo) {
assert($fileinfo instanceof \SplFileInfo); if (is_dir($dir)) {
$extension = $fileinfo->getExtension(); $iterator = new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::FOLLOW_SYMLINKS);
$filename = $fileinfo->getPathname(); $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();
$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. // There is an expectation for all modules and profiles to be loaded.
// .module and .profile files are not supposed to be in subdirectories. // .module and .profile files are not supposed to be in subdirectories.
include_once $filename;
} include_once $filename;
if ($extension === 'php') {
$cached = $hook_file_cache->get($filename);
if ($cached) {
$class = $cached['class'];
$attributes = $cached['attributes'];
} }
else { if ($extension === 'php') {
$namespace = preg_replace('#^src/#', "Drupal/$module/", $iterator->getSubPath()); $cached = $hook_file_cache->get($filename);
$class = $namespace . '/' . $fileinfo->getBasename('.php'); if ($cached) {
$class = str_replace('/', '\\', $class); $class = $cached['class'];
if (class_exists($class)) { $attributes = $cached['attributes'];
$attributes = static::getHookAttributesInClass($class);
$hook_file_cache->set($filename, ['class' => $class, 'attributes' => $attributes]);
} }
else { else {
$attributes = []; $namespace = preg_replace('#^src/#', "Drupal/$module/", $iterator->getSubPath());
$class = $namespace . '/' . $fileinfo->getBasename('.php');
$class = str_replace('/', '\\', $class);
if (class_exists($class)) {
$attributes = static::getHookAttributesInClass($class);
$hook_file_cache->set($filename, ['class' => $class, 'attributes' => $attributes]);
}
else {
$attributes = [];
}
}
foreach ($attributes as $attribute) {
$this->addFromAttribute($attribute, $class, $module);
} }
} }
foreach ($attributes as $attribute) { elseif (!$skip_procedural) {
$this->addFromAttribute($attribute, $class, $module); $implementations = $procedural_hook_file_cache->get($filename);
} if ($implementations === NULL) {
} $finder = MockFileFinder::create($filename);
else { $parser = new StaticReflectionParser('', $finder);
$implementations = $procedural_hook_file_cache->get($filename); $implementations = [];
if ($implementations === NULL) { foreach ($parser->getMethodAttributes() as $function => $attributes) {
$finder = MockFileFinder::create($filename); if (StaticReflectionParser::hasAttribute($attributes, StopProceduralHookScan::class)) {
$parser = new StaticReflectionParser('', $finder); break;
$implementations = []; }
foreach ($parser->getMethodAttributes() as $function => $attributes) { if (!StaticReflectionParser::hasAttribute($attributes, LegacyHook::class) && preg_match($module_preg, $function, $matches)) {
if (!StaticReflectionParser::hasAttribute($attributes, LegacyHook::class) && preg_match($module_preg, $function, $matches)) { $implementations[] = ['function' => $function, 'module' => $matches['module'], 'hook' => $matches['hook']];
$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']);
} }
$procedural_hook_file_cache->set($filename, $implementations);
}
foreach ($implementations as $implementation) {
$this->addProceduralImplementation($fileinfo, $implementation['hook'], $implementation['module'], $implementation['function']);
} }
} if ($extension === 'inc') {
if ($extension === 'inc') { $parts = explode('.', $fileinfo->getFilename());
$parts = explode('.', $fileinfo->getFilename()); if (count($parts) === 3 && $parts[0] === $module) {
if (count($parts) === 3 && $parts[0] === $module) { $this->groupIncludes[$parts[1]][] = $filename;
$this->groupIncludes[$parts[1]][] = $filename; }
} }
} }
} }
......
name: 'Test skipping procedural for whole module'
type: module
description: 'Test skipping procedural for whole module.'
package: Testing
version: VERSION
<?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';
}
parameters:
hook_collector_skip_procedural.hooks_converted: true
name: 'Test skipping procedural for part of the module'
type: module
description: 'Test skipping procedural for part of the module.'
package: Testing
version: VERSION
procedural_hooks: scan
<?php
/**
* @file
* Implement hooks.
*/
declare(strict_types=1);
use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
/**
* This implements a hook and should be picked up.
*
* We have set procedural_hooks: scan.
*/
function hook_collector_skip_procedural_attribute_cache_flush(): void {
// Set a global value we can check in test code.
$GLOBALS['procedural_attribute_skip_find'] = 'procedural_attribute_skip_find';
}
/**
* This implements a hook but should not be picked up.
*
* This attribute should stop all procedural hooks after.
* We implement on behalf of other modules so we can pick them up.
*/
#[StopProceduralHookScan]
function hook_collector_on_behalf_procedural_cache_flush(): void {
// Set a global value we can check in test code.
$GLOBALS['procedural_attribute_skip_has_attribute'] = 'procedural_attribute_skip_has_attribute';
}
/**
* This implements a hook but should not be picked up.
*
* The attribute above should prevent this from being found.
*/
function hook_collector_on_behalf_cache_flush(): void {
// Set a global value we can check in test code.
$GLOBALS['procedural_attribute_skip_after_attribute'] = 'procedural_attribute_skip_after_attribute';
}
...@@ -101,4 +101,26 @@ public function testHooksImplementedOnBehalfFileCache(): void { ...@@ -101,4 +101,26 @@ public function testHooksImplementedOnBehalfFileCache(): void {
$this->assertTrue(isset($GLOBALS['on_behalf_procedural'])); $this->assertTrue(isset($GLOBALS['on_behalf_procedural']));
} }
/**
* Test procedural hooks for a module are skipped when skip is set..
*/
public function testProceduralHooksSkippedWhenConfigured(): void {
$module_installer = $this->container->get('module_installer');
$this->assertTrue($module_installer->install(['hook_collector_skip_procedural']));
$this->assertTrue($module_installer->install(['hook_collector_on_behalf_procedural']));
$this->assertTrue($module_installer->install(['hook_collector_skip_procedural_attribute']));
$this->assertTrue($module_installer->install(['hook_collector_on_behalf']));
$this->assertFalse(isset($GLOBALS['skip_procedural_all']));
$this->assertFalse(isset($GLOBALS['procedural_attribute_skip_has_attribute']));
$this->assertFalse(isset($GLOBALS['procedural_attribute_skip_after_attribute']));
$this->assertFalse(isset($GLOBALS['procedural_attribute_skip_find']));
drupal_flush_all_caches();
$this->assertFalse(isset($GLOBALS['skip_procedural_all']));
$this->assertFalse(isset($GLOBALS['procedural_attribute_skip_has_attribute']));
$this->assertFalse(isset($GLOBALS['procedural_attribute_skip_after_attribute']));
// This is the only one that should be found.
$this->assertTrue(isset($GLOBALS['procedural_attribute_skip_find']));
}
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment