diff --git a/core/core.api.php b/core/core.api.php index 0500a3c58aecebc87336db728c2818d51135bc86..0dbec836754ee5f821fb1ff8ff6003238b26ca54 100644 --- a/core/core.api.php +++ b/core/core.api.php @@ -1657,6 +1657,7 @@ * Legacy meta hooks: * - hook_hook_info() * - hook_module_implements_alter() + * @see https://www.drupal.org/node/3496788 * * Install hooks: * - hook_install() diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index cf42c65deeff28e02426e1517c203e3b91b998f3..c9123822cb324f5f5c649e55388ac62ac1b207e9 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -80,11 +80,23 @@ class ModuleHandler implements ModuleHandlerInterface { * An array keyed by hook, classname, method and the value is the module. * @param array $groupIncludes * An array of .inc files to get helpers from. + * @param array<string, list<string>> $orderedExtraTypes + * A multidimensional array of hooks that have been ordered and the + * extra_types they have been ordered against. This is stored separately + * from $hookImplementationsMap to prevent ordering again since this set + * has already been fully ordered in HookCollectorPass. * * @see \Drupal\Core\DrupalKernel * @see \Drupal\Core\CoreServiceProvider */ - public function __construct($root, array $module_list, protected EventDispatcherInterface $eventDispatcher, protected array $hookImplementationsMap, protected array $groupIncludes = []) { + public function __construct( + $root, + array $module_list, + protected EventDispatcherInterface $eventDispatcher, + protected array $hookImplementationsMap, + protected array $groupIncludes = [], + protected array $orderedExtraTypes = [], + ) { $this->root = $root; $this->moduleList = []; foreach ($module_list as $name => $module) { @@ -192,7 +204,8 @@ protected function add($type, $name, $path) { $filename = file_exists($php_file_path) ? "$name.$type" : NULL; $this->moduleList[$name] = new Extension($this->root, $type, $pathname, $filename); $this->resetImplementations(); - $hook_collector = HookCollectorPass::collectAllHookImplementations([$name => ['pathname' => $pathname]]); + $paths = [$name => ['pathname' => $pathname]]; + $hook_collector = HookCollectorPass::collectAllHookImplementations($paths); // A module freshly added will not be registered on the container yet. // ProceduralCall service does not yet know about it. // Note in HookCollectorPass: @@ -200,7 +213,7 @@ protected function add($type, $name, $path) { // Load all includes so the legacy section of invoke can handle hooks in includes. $hook_collector->loadAllIncludes(); // Register procedural implementations. - foreach ($hook_collector->getImplementations() as $hook => $moduleImplements) { + foreach ($hook_collector->getImplementations($paths) as $hook => $moduleImplements) { foreach ($moduleImplements as $module => $classImplements) { foreach ($classImplements[ProceduralCall::class] ?? [] as $method) { $this->invokeMap[$hook][$module][] = $method; @@ -427,27 +440,38 @@ public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) { $this->alterEventListeners[$cid] = []; $hook = $type . '_alter'; $hook_listeners = $this->getHookListeners($hook); + $extra_modules = FALSE; + $extra_listeners = []; if (isset($extra_types)) { - // For multiple hooks, we need $modules to contain every module that - // implements at least one of them in the correct order. - foreach ($extra_types as $extra_type) { - foreach ($this->getHookListeners($extra_type . '_alter') as $module => $listeners) { - if (isset($hook_listeners[$module])) { - $hook_listeners[$module] = array_merge($hook_listeners[$module], $listeners); - } - else { - $hook_listeners[$module] = $listeners; - $extra_modules = TRUE; - } + $extra_hooks = array_map(static fn ($x) => $x . '_alter', $extra_types); + // First get the listeners implementing extra hooks. + foreach ($extra_hooks as $extra_hook) { + $hook_listeners = $this->findListenersForAlter($extra_hook, $hook_listeners, $extra_modules); + } + // Second, gather implementations ordered together. These are only used + // for ordering because the set might contain hooks not included in + // this alter() call. \Drupal\Core\Hook\HookPriority::change() + // registers the implementations of combined hooks. + foreach ([...$extra_hooks, $hook] as $extra_hook) { + if (isset($this->orderedExtraTypes[$extra_hook])) { + $orderedHooks = $this->orderedExtraTypes[$extra_hook]; + $extra_listeners = $this->findListenersForAlter(implode(':', $orderedHooks)); + // Remove already ordered hooks. + $extra_hooks = array_diff($extra_hooks, $orderedHooks); } } } - // If any modules implement one of the extra hooks that do not implement - // the primary hook, we need to add them to the $modules array in their - // appropriate order. - $modules = array_keys($hook_listeners); - if (isset($extra_modules)) { - $modules = $this->reOrderModulesForAlter($modules, $hook); + // If multiple alters were called, but they were already ordered by + // ordering attributes then keep that order. + if (isset($extra_hooks) && empty($extra_hooks)) { + $modules = array_keys(array_intersect_key($extra_listeners, $hook_listeners)); + } + else { + // Otherwise, use a legacy ordering mechanism if needed. + $modules = array_keys($hook_listeners); + if ($extra_modules) { + $modules = $this->legacyReOrderModulesForAlter($modules, $hook); + } } foreach ($modules as $module) { foreach ($hook_listeners[$module] ?? [] as $listener) { @@ -472,7 +496,7 @@ public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) { * The list, potentially reordered and changed by * hook_module_implements_alter(). */ - protected function reOrderModulesForAlter(array $modules, string $hook): array { + protected function legacyReOrderModulesForAlter(array $modules, string $hook): array { // Order by module order first. $modules = array_intersect(array_keys($this->moduleList), $modules); // Alter expects the module list to be in the keys. @@ -572,7 +596,36 @@ protected function getHookListeners(string $hook): array { } } } + return $this->invokeMap[$hook] ?? []; } + /** + * Helper to get hook listeners when in alter. + * + * @param string $hook + * The extra hook or combination hook to check for. + * @param array<string, list<callable>> $hook_listeners + * Hook listeners for the current hook_alter. + * @param bool|null $extra_modules + * Whether there are extra modules to order. + * + * @return array<string, list<callable>> + * The hook listeners. + */ + public function findListenersForAlter(string $hook, array $hook_listeners = [], ?bool &$extra_modules = NULL): array { + foreach ($this->getHookListeners($hook) as $module => $listeners) { + if (isset($hook_listeners[$module])) { + $hook_listeners[$module] = array_merge($hook_listeners[$module], $listeners); + } + else { + $hook_listeners[$module] = $listeners; + // It is used by reference. + // @phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis.UnusedVariable + $extra_modules = TRUE; + } + } + return $hook_listeners; + } + } diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php index 765992e0606d247e4556e3b54c5c362cddd2903f..d44b3d4f1c2ff6ef7280c0ce17e02e4cea82ae6f 100644 --- a/core/lib/Drupal/Core/Extension/module.api.php +++ b/core/lib/Drupal/Core/Extension/module.api.php @@ -94,6 +94,15 @@ function hook_hook_info(): array { /** * Alter the registry of modules implementing a hook. * + * This hook will be removed in 12.0.0. + * It has been intentionally not deprecated because custom code and contributed + * modules will still need to maintain implementations with the + * #[LegacyModuleImplementsAlter] attribute in order to support drupal versions + * older than 11.2.0. + * + * @link https://www.drupal.org/node/3496788 + * + * * Only procedural implementations are supported for this hook. * * This hook is invoked in \Drupal::moduleHandler()->getImplementationInfo(). @@ -115,6 +124,8 @@ function hook_hook_info(): array { * file named $module.$group.inc. * @param string $hook * The name of the module hook being implemented. + * + * @see https://www.drupal.org/node/3496788 */ function hook_module_implements_alter(&$implementations, $hook) { if ($hook == 'form_alter') { diff --git a/core/lib/Drupal/Core/Hook/Attribute/Hook.php b/core/lib/Drupal/Core/Hook/Attribute/Hook.php index 1b220577a13130d6b8a790a5b1f41ecaebb60fd7..0a4c5b5cdec9c234917d0865bfd119a8fd7976ac 100644 --- a/core/lib/Drupal/Core/Hook/Attribute/Hook.php +++ b/core/lib/Drupal/Core/Hook/Attribute/Hook.php @@ -4,12 +4,17 @@ namespace Drupal\Core\Hook\Attribute; +use Drupal\Core\Hook\ComplexOrder; +use Drupal\Core\Hook\HookOperation; +use Drupal\Core\Hook\Order; + /** * Attribute for defining a class method as a hook implementation. * * Hook implementations in classes need to be marked with this attribute, * using one of the following techniques: * - On a method, use this attribute with the hook name: + * * @code * #[Hook('user_cancel')] * public function userCancel(...) {} @@ -30,8 +35,14 @@ * } * @endcode * - * Ordering hook implementations can be done by implementing - * hook_module_implements_alter. + * Ordering hook implementations can be done by using the order parameter. + * + * @see https://www.drupal.org/node/3493962 + * + * Removing hook implementations can be done by using the attribute + * \Drupal\Core\Hook\Attribute/RemoveHook. + * + * @see https://www.drupal.org/node/3496786 * * Classes that use this annotation on the class or on their methods are * automatically registered as autowired services with the class name as the @@ -86,9 +97,11 @@ * the procedural hook implementations. * * See \Drupal\Core\Hook\Attribute\LegacyHook for additional information. + * + * @internal */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] -class Hook { +class Hook extends HookOperation { /** * Constructs a Hook attribute object. @@ -104,23 +117,38 @@ class Hook { * (optional) The module this implementation is for. This allows one module * to implement a hook on behalf of another module. Defaults to the module * the implementation is in. + * @param \Drupal\Core\Hook\Order|\Drupal\Core\Hook\ComplexOrder|null $order + * (optional) Set the order of the implementation. */ public function __construct( - public string $hook, - public string $method = '', + string $hook, + string $method = '', public ?string $module = NULL, - ) {} + Order|ComplexOrder|null $order = NULL, + ) { + parent::__construct($hook, $method, order: $order); + } /** - * Set the method the hook should apply to. + * Set necessary parameters for the hook attribute. * + * @param class-string $class + * The class for the hook. + * @param string $module + * The module for the hook. * @param string $method - * The method that the hook attribute applies to. - * This only needs to be set when the attribute is on the class. + * The method for the hook. */ - public function setMethod(string $method): static { - $this->method = $method; - return $this; + public function set(string $class, string $module, string $method): void { + if (!$this->class) { + $this->class = $class; + } + if (!$this->module) { + $this->module = $module; + } + if (!$this->method) { + $this->method = $method; + } } } diff --git a/core/lib/Drupal/Core/Hook/Attribute/LegacyHook.php b/core/lib/Drupal/Core/Hook/Attribute/LegacyHook.php index ee6501d7b42e7e57679002a651eb54dcfea32492..4b2726a635c2704cf6491b3a684273bedfe5dc5d 100644 --- a/core/lib/Drupal/Core/Hook/Attribute/LegacyHook.php +++ b/core/lib/Drupal/Core/Hook/Attribute/LegacyHook.php @@ -19,6 +19,8 @@ * only the legacy hook implementation is executed. * * For more information, see https://www.drupal.org/node/3442349. + * + * @internal */ #[\Attribute(\Attribute::TARGET_FUNCTION)] class LegacyHook { diff --git a/core/lib/Drupal/Core/Hook/Attribute/LegacyModuleImplementsAlter.php b/core/lib/Drupal/Core/Hook/Attribute/LegacyModuleImplementsAlter.php new file mode 100644 index 0000000000000000000000000000000000000000..0940fdc012bd019570170eae7e43e6318cc538e0 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/Attribute/LegacyModuleImplementsAlter.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook\Attribute; + +/** + * Defines a LegacyModuleImplementsAlter object. + * + * This allows contrib and core to maintain legacy hook_module_implements_alter + * alongside the new attribute-based ordering. This means that a contrib module + * can simultaneously support Drupal 11.2 and older versions of Drupal. + * + * Marking hook_module_implements_alter as #LegacyModuleImplementsAlter will + * prevent hook_module_implements_alter from running when attribute-based + * ordering is available. + * + * On older versions of Drupal which are not aware of attribute-based ordering, + * only the legacy hook implementation is executed. + * + * For more information, see https://www.drupal.org/node/3496788. + * + * @internal + */ +#[\Attribute(\Attribute::TARGET_FUNCTION)] +class LegacyModuleImplementsAlter {} diff --git a/core/lib/Drupal/Core/Hook/Attribute/ReOrderHook.php b/core/lib/Drupal/Core/Hook/Attribute/ReOrderHook.php new file mode 100644 index 0000000000000000000000000000000000000000..1f88da580f4bdbc7cfb0cec0e97e42e89fbcb579 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/Attribute/ReOrderHook.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook\Attribute; + +use Drupal\Core\Hook\ComplexOrder; +use Drupal\Core\Hook\HookOperation; +use Drupal\Core\Hook\Order; + +/** + * Set the order of an already existing implementation. + * + * @internal + */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class ReOrderHook extends HookOperation { + + /** + * Constructs a ReOrderHook object. + * + * @param string $hook + * The hook parameter of the #Hook being modified. + * @param class-string $class + * The class the implementation to modify is in. + * @param string $method + * The method name of the #Hook being modified. If the hook attribute is + * on a class and does not have method set, then use __invoke. + * @param \Drupal\Core\Hook\Order|\Drupal\Core\Hook\ComplexOrder $order + * Set the order of the implementation. + */ + public function __construct( + string $hook, + string $class, + string $method, + Order|ComplexOrder $order, + ) { + parent::__construct($hook, $method, $class, $order); + } + +} diff --git a/core/lib/Drupal/Core/Hook/Attribute/RemoveHook.php b/core/lib/Drupal/Core/Hook/Attribute/RemoveHook.php new file mode 100644 index 0000000000000000000000000000000000000000..5b4cca3b132a96bb8d2a8b9a681e94915518db19 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/Attribute/RemoveHook.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook\Attribute; + +use Drupal\Core\Hook\HookOperation; + +/** + * Attribute for removing an implementation. + * + * @internal + */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class RemoveHook extends HookOperation { + + /** + * Constructs a RemoveHook object. + * + * @param string $hook + * The hook parameter of the #Hook being modified. + * @param class-string $class + * The class the implementation to modify is in. + * @param string $method + * The method name of the #Hook being modified. If the hook attribute is + * on a class and does not have method set, then use __invoke. + */ + public function __construct( + string $hook, + string $class, + string $method, + ) { + parent::__construct($hook, $method, $class); + } + +} diff --git a/core/lib/Drupal/Core/Hook/Attribute/StopProceduralHookScan.php b/core/lib/Drupal/Core/Hook/Attribute/StopProceduralHookScan.php index 73f0ce6915bd37e3ff5476638893af4c910f7c2f..cc41fff51533a8c2a9f80ff1edfcccfdeef82ae2 100644 --- a/core/lib/Drupal/Core/Hook/Attribute/StopProceduralHookScan.php +++ b/core/lib/Drupal/Core/Hook/Attribute/StopProceduralHookScan.php @@ -9,6 +9,8 @@ * * This allows contrib and core to mark when a file has no more * procedural hooks. + * + * @internal */ #[\Attribute(\Attribute::TARGET_FUNCTION)] class StopProceduralHookScan { diff --git a/core/lib/Drupal/Core/Hook/ComplexOrder.php b/core/lib/Drupal/Core/Hook/ComplexOrder.php new file mode 100644 index 0000000000000000000000000000000000000000..a44e0bd2e815a2c560181ca1abf5efa28edef99f --- /dev/null +++ b/core/lib/Drupal/Core/Hook/ComplexOrder.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook; + +/** + * Set this implementation to be before or after others. + */ +abstract readonly class ComplexOrder { + + /** + * Whether the priority of this hook should be larger than others. + */ + const bool VALUE = FALSE; + + /** + * Whether the priority of this hook should be larger than others. + * + * This is fixed to the constant ::VALUE, it simplifies ordering by ensuring + * ComplexOrder and Order types both have a value property. + * + * @var bool + */ + public bool $value; + + /** + * Constructs a ComplexOrder object. + * + * @param list<string> $modules + * A list of modules. + * @param list<array{class-string, string}> $classesAndMethods + * A list of classes and methods, for example: + * @code + * [ + * [Foo::class, 'someMethod'], + * [Bar::class, 'someOtherMethod'], + * ] + * @endcode + * @param list<string> $extraTypes + * A list of hooks to be ordered together. Ordering by attributes happens + * at build time by setting up the order of the listeners of a hook + * correctly. However, ModuleHandlerInterface::alter() can be called with + * multiple hooks runtime. If the hook defined on this method/class + * requires ordering relative to other such hooks then this parameter can + * be used to order relative to implementations of all hooks in the set. + * Include all alter hooks to be ordered against in the set even if no + * single alter() call includes all of them. For example, this can be used + * to order a hook_form_BASE_FORM_ID_alter() implementation relative to + * multiple hook_form_FORM_ID_alter() implementations as + * Drupal\ckeditor5\Hook\Ckeditor5Hooks::formFilterFormatFormAlter() does. + */ + public function __construct( + public array $modules = [], + public array $classesAndMethods = [], + public array $extraTypes = [], + ) { + if (!$this->modules && !$this->classesAndMethods) { + throw new \LogicException('Order must provide either modules or class-method pairs to order against.'); + } + $this->value = static::VALUE; + } + +} diff --git a/core/lib/Drupal/Core/Hook/HookCollectorPass.php b/core/lib/Drupal/Core/Hook/HookCollectorPass.php index 3809e24af21d56b20f034b2a45848de6d47e6e93..02d78fe8be4df76891477ebb4dcc8c171cd3c0b2 100644 --- a/core/lib/Drupal/Core/Hook/HookCollectorPass.php +++ b/core/lib/Drupal/Core/Hook/HookCollectorPass.php @@ -10,6 +10,9 @@ use Drupal\Core\Extension\ProceduralCall; use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Hook\Attribute\LegacyHook; +use Drupal\Core\Hook\Attribute\LegacyModuleImplementsAlter; +use Drupal\Core\Hook\Attribute\ReOrderHook; +use Drupal\Core\Hook\Attribute\RemoveHook; use Drupal\Core\Hook\Attribute\StopProceduralHookScan; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -29,22 +32,6 @@ */ 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. * @@ -71,50 +58,281 @@ class HookCollectorPass implements CompilerPassInterface { */ private array $groupIncludes = []; + /** + * Implementations, as module names keyed by hook name and "$class::$method". + * + * @var array<string, array<string, string>> + */ + protected array $implementations = []; + + /** + * @var array<int, list<\Drupal\Core\Hook\HookOperation>> + */ + protected array $orderAttributesByPhase = [0 => [], 1 => []]; + + /** + * @var list<\Drupal\Core\Hook\Attribute\RemoveHook> + */ + protected array $removeHookAttributes = []; + /** * {@inheritdoc} */ public function process(ContainerBuilder $container): void { - $collector = static::collectAllHookImplementations($container->getParameter('container.modules'), $container); - $map = []; + $module_list = $container->getParameter('container.modules'); + $parameters = $container->getParameterBag()->all(); + $skip_procedural_modules = array_filter( + array_keys($module_list), + fn (string $module) => !empty($parameters["$module.hooks_converted"]), + ); + $collector = static::collectAllHookImplementations($module_list, $skip_procedural_modules); + + $collector->writeToContainer($container); + } + + /** + * Writes collected definitions to the container builder. + * + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * Container builder. + */ + protected function writeToContainer(ContainerBuilder $container): void { + $orderExtraTypes = $this->getOrderExtraTypes(); + $container->register(ProceduralCall::class, ProceduralCall::class) - ->addArgument($collector->includes); + ->addArgument($this->includes); + + // Gather includes for each hook_hook_info group. + // We store this in $groupIncludes so moduleHandler can ensure the files + // are included runtime when the hooks are invoked. $groupIncludes = []; - foreach ($collector->hookInfo as $function) { + foreach ($this->hookInfo as $function) { foreach ($function() as $hook => $info) { - if (isset($collector->groupIncludes[$info['group']])) { - $groupIncludes[$hook] = $collector->groupIncludes[$info['group']]; + if (isset($this->groupIncludes[$info['group']])) { + $groupIncludes[$hook] = $this->groupIncludes[$info['group']]; } } } + + $implementationsByHook = $this->calculateImplementations($orderExtraTypes); + + static::writeImplementationsToContainer($container, $implementationsByHook); + + // Update the module handler definition. $definition = $container->getDefinition('module_handler'); $definition->setArgument('$groupIncludes', $groupIncludes); - foreach ($collector->moduleImplements as $hook => $moduleImplements) { - foreach ($collector->moduleImplementsAlters as $alter) { + $definition->setArgument('$orderedExtraTypes', $orderExtraTypes); + } + + /** + * Gets implementation lists with removals already applied. + * + * @return array<string, list<string>> + * Implementations, as module names keyed by hook name and + * "$class::$method". + */ + protected function getFilteredImplementations(): array { + $implementationsByHook = $this->implementations; + foreach ($this->removeHookAttributes as $removeHook) { + unset($implementationsByHook[$removeHook->hook][$removeHook->class . '::' . $removeHook->method]); + } + return $implementationsByHook; + } + + /** + * Gets groups of extra hooks from collected data. + * + * @return array<string, list<string>> + * Lists of extra hooks keyed by main hook. + */ + protected function getOrderExtraTypes(): array { + // Loop over all ReOrderHook attributes and gather order information + // before registering the hooks. This must happen after all collection, + // but before registration to ensure this ordering directive takes + // precedence. + /** @var list<\Drupal\Core\Hook\HookOperation> $hookOrderOperations */ + $hookOrderOperations = array_merge(...$this->orderAttributesByPhase); + $orderExtraTypes = []; + foreach ($hookOrderOperations as $hookWithOrder) { + if ($hookWithOrder->order instanceof ComplexOrder && $hookWithOrder->order->extraTypes) { + $extraTypes = [... $hookWithOrder->order->extraTypes, $hookWithOrder->hook]; + foreach ($extraTypes as $extraHook) { + $orderExtraTypes[$extraHook] = array_merge($orderExtraTypes[$extraHook] ?? [], $extraTypes); + } + } + } + $orderExtraTypes = array_map('array_unique', $orderExtraTypes); + return array_map('array_values', $orderExtraTypes); + } + + /** + * Calculates the ordered implementations. + * + * @param array<string, list<string>> $orderExtraTypes + * Extra types to order a hook with. + * + * @return array<string, array<string, string>> + * Implementations, as module names keyed by hook name and "$class::$method" + * identifier. + */ + protected function calculateImplementations(array $orderExtraTypes): array { + $implementationsByHookOrig = $this->getFilteredImplementations(); + + // List of hooks and modules formatted for hook_module_implements_alter(). + $moduleImplementsMap = []; + foreach ($implementationsByHookOrig as $hook => $hookImplementations) { + foreach ($hookImplementations as $module) { + $moduleImplementsMap[$hook][$module] = ''; + } + } + + $implementationsByHook = []; + foreach ($moduleImplementsMap as $hook => $moduleImplements) { + $extraHooks = $orderExtraTypes[$hook] ?? []; + // Add implementations to the array we pass to legacy ordering + // when the definition specifies that they should be ordered together. + foreach ($extraHooks as $extraHook) { + $moduleImplements += $moduleImplementsMap[$extraHook] ?? []; + } + // Process all hook_module_implements_alter() for build time ordering. + foreach ($this->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--, - ]); + foreach (array_keys($implementationsByHookOrig[$hook], $module, TRUE) as $identifier) { + $implementationsByHook[$hook][$identifier] = $module; + } + if (count($extraHooks) > 1) { + $combinedHook = implode(':', $extraHooks); + foreach ($extraHooks as $extraHook) { + foreach (array_keys($implementationsByHookOrig[$extraHook] ?? [], $module, TRUE) as $identifier) { + $implementationsByHook[$combinedHook][$identifier] = $module; + } } } } } + + /** @var list<\Drupal\Core\Hook\HookOperation> $hookOrderOperations */ + $hookOrderOperations = array_merge(...$this->orderAttributesByPhase); + foreach ($hookOrderOperations as $hookOrderOperation) { + static::applyOrderAttributeOperation( + $implementationsByHook, + $orderExtraTypes, + $hookOrderOperation, + ); + } + + return $implementationsByHook; + } + + /** + * Applies hook order changes from a single attribute with order information. + * + * @param array<string, array<string, string>> $implementationsByHook + * Implementations, as module names keyed by hook name and "$class::$method" + * identifier. + * @param array<string, list<string>> $orderExtraTypes + * Extra types to order a hook with. + * @param \Drupal\Core\Hook\HookOperation $hookOrderOperation + * Hook attribute with order information. + */ + protected static function applyOrderAttributeOperation( + array &$implementationsByHook, + array $orderExtraTypes, + HookOperation $hookOrderOperation, + ): void { + // ::process() adds the hook serving as key to the order extraTypes so it + // does not need to be added if there's a extraTypes for the hook. + $hooks = $orderExtraTypes[$hookOrderOperation->hook] ?? [$hookOrderOperation->hook]; + $combinedHook = implode(':', $hooks); + $identifier = $hookOrderOperation->class . '::' . $hookOrderOperation->method; + $module = $implementationsByHook[$combinedHook][$identifier] ?? NULL; + if ($module === NULL) { + // Implementation is not in the list. Nothing to reorder. + return; + } + $list = $implementationsByHook[$combinedHook]; + $order = $hookOrderOperation->order; + if ($order === NULL) { + throw new \InvalidArgumentException('This method must only be called with attributes that have order information.'); + } + if ($order === Order::First) { + unset($list[$identifier]); + $list = [$identifier => $module] + $list; + } + elseif ($order === Order::Last) { + unset($list[$identifier]); + $list[$identifier] = $module; + } + elseif ($order instanceof ComplexOrder) { + $shouldBeAfter = !$order->value; + unset($list[$identifier]); + $identifiers = array_keys($list); + $modules = array_values($list); + $compareIndices = []; + if (isset($hookOrderOperation->order->modules)) { + $compareIndices = array_keys(array_intersect($modules, $hookOrderOperation->order->modules)); + } + foreach ($hookOrderOperation->order->classesAndMethods as [$otherClass, $otherMethod]) { + $compareIndices[] = array_search("$otherClass::$otherMethod", $identifiers, TRUE); + } + if (!$compareIndices) { + return; + } + $splice_index = $shouldBeAfter + ? max($compareIndices) + 1 + : min($compareIndices); + array_splice($identifiers, $splice_index, 0, [$identifier]); + array_splice($modules, $splice_index, 0, [$module]); + $list = array_combine($identifiers, $modules); + } + $implementationsByHook[$combinedHook] = $list; + } + + /** + * Writes all implementations to the container. + * + * @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" + * identifier. + */ + protected static function writeImplementationsToContainer( + ContainerBuilder $container, + array $implementationsByHook, + ): void { + $map = []; + $tagsInfoByClass = []; + foreach ($implementationsByHook as $hook => $hookImplementations) { + $priority = 0; + foreach ($hookImplementations as $class_and_method => $module) { + [$class, $method] = explode('::', $class_and_method); + $tagsInfoByClass[$class][] = [ + 'event' => "drupal_hook.$hook", + 'method' => $method, + 'priority' => $priority, + ]; + --$priority; + $map[$hook][$class][$method] = $module; + } + } + + foreach ($tagsInfoByClass as $class => $tagsInfo) { + if ($container->hasDefinition($class)) { + $definition = $container->findDefinition($class); + } + else { + $definition = $container + ->register($class, $class) + ->setAutowired(TRUE); + } + foreach ($tagsInfo as $tag_info) { + $definition->addTag('kernel.event_listener', $tag_info); + } + } + $container->setParameter('hook_implementations_map', $map); } @@ -124,8 +342,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. + * @param list<string> $skipProceduralModules + * Module names that are known to not have procedural hook implementations. * * @return static * A HookCollectorPass instance holding all hook implementations and @@ -134,20 +352,17 @@ 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 + * @todo Pass only $container when ModuleHandler::add() is removed + * @see https://www.drupal.org/project/drupal/issues/3481778 */ - public static function collectAllHookImplementations(array $module_filenames, ?ContainerBuilder $container = NULL): static { - $modules = array_map(fn ($x) => preg_quote($x, '/'), array_keys($module_filenames)); + public static function collectAllHookImplementations(array $module_filenames, array $skipProceduralModules = []): static { + $modules = array_map(static 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) { - $skip_procedural = FALSE; - if ($container?->hasParameter("$module.hooks_converted")) { - $skip_procedural = $container->getParameter("$module.hooks_converted"); - } + $skip_procedural = in_array($module, $skipProceduralModules); $collector->collectModuleHookImplementations(dirname($info['pathname']), $module, $module_preg, $skip_procedural); } return $collector; @@ -195,16 +410,30 @@ protected function collectModuleHookImplementations($dir, $module, $module_preg, $namespace = preg_replace('#^src/#', "Drupal/$module/", $iterator->getSubPath()); $class = $namespace . '/' . $fileinfo->getBasename('.php'); $class = str_replace('/', '\\', $class); + $attributes = []; if (class_exists($class)) { - $attributes = static::getHookAttributesInClass($class); + $reflectionClass = new \ReflectionClass($class); + $attributes = self::getAttributeInstances($reflectionClass); $hook_file_cache->set($filename, ['class' => $class, 'attributes' => $attributes]); } - else { - $attributes = []; - } } - foreach ($attributes as $attribute) { - $this->addFromAttribute($attribute, $class, $module); + foreach ($attributes as $method => $methodAttributes) { + foreach ($methodAttributes as $attribute) { + if ($attribute instanceof Hook) { + self::checkForProceduralOnlyHooks($attribute, $class); + $this->implementations[$attribute->hook][$class . '::' . ($attribute->method ?: $method)] = $attribute->module ?? $module; + if ($attribute->order !== NULL) { + $attribute->set($class, $attribute->module ?? $module, $method); + $this->orderAttributesByPhase[0][] = $attribute; + } + } + elseif ($attribute instanceof ReOrderHook) { + $this->orderAttributesByPhase[1][] = $attribute; + } + elseif ($attribute instanceof RemoveHook) { + $this->removeHookAttributes[] = $attribute; + } + } } } elseif (!$skip_procedural) { @@ -217,7 +446,7 @@ protected function collectModuleHookImplementations($dir, $module, $module_preg, if (StaticReflectionParser::hasAttribute($attributes, StopProceduralHookScan::class)) { break; } - if (!StaticReflectionParser::hasAttribute($attributes, LegacyHook::class) && preg_match($module_preg, $function, $matches)) { + if (!StaticReflectionParser::hasAttribute($attributes, LegacyHook::class) && preg_match($module_preg, $function, $matches) && !StaticReflectionParser::hasAttribute($attributes, LegacyModuleImplementsAlter::class)) { $implementations[] = ['function' => $function, 'module' => $matches['module'], 'hook' => $matches['hook']]; } } @@ -255,84 +484,29 @@ protected static function filterIterator(\SplFileInfo $fileInfo, $key, \Recursiv 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 { - $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 string $class - * The class in which said attribute resides in. - * @param string $module - * The module in which the class resides in. - */ - protected function addFromAttribute(Hook $hook, $class, $module): void { - 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) + * The file this procedural implementation is in. * @param string $hook - * The name of the hook. (Huh, right?) + * The name of the hook. * @param string $module - * The name of the module. (Truly shocking!) + * The module of the hook. Note this might be different from the module the + * function is in. * @param string $function - * The name of function implementing the hook. (Wow!) + * The name of function implementing the hook. */ protected function addProceduralImplementation(\SplFileInfo $fileinfo, string $hook, string $module, string $function): void { - $this->addFromAttribute(new Hook($hook, $module . '_' . $hook), ProceduralCall::class, $module); if ($hook === 'hook_info') { $this->hookInfo[] = $function; } - if ($hook === 'module_implements_alter') { + elseif ($hook === 'module_implements_alter') { + $message = "$function without a #[LegacyModuleImplementsAlter] attribute is deprecated in drupal:11.2.0 and removed in drupal:12.0.0. See https://www.drupal.org/node/3496788"; + @trigger_error($message, E_USER_DEPRECATED); $this->moduleImplementsAlters[] = $function; } + $this->implementations[$hook][ProceduralCall::class . '::' . $module . '_' . $hook] = $module; if ($fileinfo->getExtension() !== 'module') { $this->includes[$function] = $fileinfo->getPathname(); } @@ -341,6 +515,9 @@ protected function addProceduralImplementation(\SplFileInfo $fileinfo, string $h /** * This method is only to be used by ModuleHandler. * + * @todo remove when ModuleHandler::add() is removed. + * @see https://www.drupal.org/project/drupal/issues/3481778 + * * @internal */ public function loadAllIncludes(): void { @@ -352,21 +529,45 @@ public function loadAllIncludes(): void { /** * This method is only to be used by ModuleHandler. * + * @param array<string, array{pathname: string}> $paths + * Reduced module info arrays by module name. + * + * @return array<string, array<string, array<class-string, array<string, string>>>> + * Hook implementation method names keyed by hook, module, class and method. + * + * @todo remove when ModuleHandler::add() is removed. + * See https://www.drupal.org/project/drupal/issues/3481778 + * * @internal */ - public function getImplementations(): array { - return $this->implementations; + public function getImplementations(array $paths): array { + $implementationsByHook = $this->getFilteredImplementations(); + + // List of modules implementing hooks with the implementation details. + $implementations = []; + + $modules = array_keys($paths); + foreach ($implementationsByHook as $hook => $hookImplementations) { + foreach ($modules as $module) { + foreach (array_keys($hookImplementations, $module, TRUE) as $identifier) { + [$class, $method] = explode('::', $identifier); + $implementations[$hook][$module][$class][$method] = $method; + } + } + } + + return $implementations; } /** * Checks for hooks which can't be supported in classes. * - * @param \Drupal\Core\Hook\Attribute\Hook $hook + * @param \Drupal\Core\Hook\Attribute\Hook $hookAttribute * The hook to check. - * @param string $class + * @param class-string $class * The class the hook is implemented on. */ - public static function checkForProceduralOnlyHooks(Hook $hook, string $class): void { + public static function checkForProceduralOnlyHooks(Hook $hookAttribute, string $class): void { $staticDenyHooks = [ 'hook_info', 'install', @@ -379,9 +580,31 @@ public static function checkForProceduralOnlyHooks(Hook $hook, string $class): v 'install_tasks_alter', ]; - if (in_array($hook->hook, $staticDenyHooks) || preg_match('/^(post_update_|preprocess_|update_\d+$)/', $hook->hook)) { - throw new \LogicException("The hook $hook->hook on class $class does not support attributes and must remain procedural."); + if (in_array($hookAttribute->hook, $staticDenyHooks) || preg_match('/^(post_update_|preprocess_|update_\d+$)/', $hookAttribute->hook)) { + throw new \LogicException("The hook $hookAttribute->hook on class $class does not support attributes and must remain procedural."); + } + } + + /** + * Get attribute instances from class and method reflections. + * + * @param \ReflectionClass $reflectionClass + * A reflected class. + * + * @return array<string, list<\Drupal\Core\Hook\HookOperation>> + * 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(HookOperation::class, \ReflectionAttribute::IS_INSTANCEOF)) { + $method = $reflection instanceof \ReflectionMethod ? $reflection->getName() : '__invoke'; + $attributes[$method] = array_map(static fn (\ReflectionAttribute $ra) => $ra->newInstance(), $reflectionAttributes); + } } + return $attributes; } } diff --git a/core/lib/Drupal/Core/Hook/HookOperation.php b/core/lib/Drupal/Core/Hook/HookOperation.php new file mode 100644 index 0000000000000000000000000000000000000000..f212588a42c631458377963b909b681d80b73de3 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/HookOperation.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook; + +/** + * Base class for attributes that affect or define hook implementations. + * + * @internal + */ +abstract class HookOperation { + + /** + * Constructs a HookOperation object. + * + * @param string $hook + * The hook being implemented or modified. + * @param string $method + * The method for the hook being implemented or modified. + * This is required when modifying existing hook implementations it is + * optional otherwise. See \Drupal\Core\Hook\Attribute\Hook for more + * information. + * @param class-string $class + * (optional) The class of the hook being implemented or modified. + * @param \Drupal\Core\Hook\Order|\Drupal\Core\Hook\ComplexOrder|null $order + * (optional) Set the order of the hook referenced. + */ + public function __construct( + public string $hook, + public string $method, + public ?string $class = NULL, + public Order|ComplexOrder|null $order = NULL, + ) {} + +} diff --git a/core/lib/Drupal/Core/Hook/Order.php b/core/lib/Drupal/Core/Hook/Order.php new file mode 100644 index 0000000000000000000000000000000000000000..f26ef257db72e980a4aafdee2d244a8e77b46d6b --- /dev/null +++ b/core/lib/Drupal/Core/Hook/Order.php @@ -0,0 +1,18 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook; + +/** + * Set this implementation to be first or last. + */ +enum Order: int { + + // This implementation should fire first. + case First = 1; + + // This implementation should fire last. + case Last = 0; + +} diff --git a/core/lib/Drupal/Core/Hook/OrderAfter.php b/core/lib/Drupal/Core/Hook/OrderAfter.php new file mode 100644 index 0000000000000000000000000000000000000000..be7811c1b79cdfdb32b3ceab76764a402d4d468f --- /dev/null +++ b/core/lib/Drupal/Core/Hook/OrderAfter.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook; + +/** + * Set this implementation to be after others. + */ +readonly class OrderAfter extends ComplexOrder { + + /** + * After means the priority should not be larger than others. + */ + const bool VALUE = FALSE; + +} diff --git a/core/lib/Drupal/Core/Hook/OrderBefore.php b/core/lib/Drupal/Core/Hook/OrderBefore.php new file mode 100644 index 0000000000000000000000000000000000000000..4b1a1df6208b781fa061a66af3e2bfe1d0c0b505 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/OrderBefore.php @@ -0,0 +1,17 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook; + +/** + * Set this implementation to be before others. + */ +readonly class OrderBefore extends ComplexOrder { + + /** + * Before means the priority should be larger than others. + */ + const bool VALUE = TRUE; + +} diff --git a/core/modules/ckeditor5/ckeditor5.module b/core/modules/ckeditor5/ckeditor5.module index 551ff7f397eaf66128a296d7653025adb4f26b16..b8db617e090d9661daef2617fc573b82a13cf778 100644 --- a/core/modules/ckeditor5/ckeditor5.module +++ b/core/modules/ckeditor5/ckeditor5.module @@ -17,31 +17,6 @@ use Drupal\Core\Ajax\RemoveCommand; use Drupal\Core\Form\FormStateInterface; -/** - * Implements hook_module_implements_alter(). - */ -function ckeditor5_module_implements_alter(&$implementations, $hook): void { - // This module's implementation of form_filter_format_form_alter() must happen - // after the editor module's implementation, as that implementation adds the - // active editor to $form_state. It must also happen after the media module's - // implementation so media_filter_format_edit_form_validate can be removed - // from the validation chain, as that validator is not needed with CKEditor 5 - // and will trigger a false error. - if ($hook === 'form_alter' && isset($implementations['ckeditor5']) && isset($implementations['editor'])) { - $group = $implementations['ckeditor5']; - unset($implementations['ckeditor5']); - - $offset = array_search('editor', array_keys($implementations)) + 1; - if (array_key_exists('media', $implementations)) { - $media_offset = array_search('media', array_keys($implementations)) + 1; - $offset = max([$offset, $media_offset]); - } - $implementations = array_slice($implementations, 0, $offset, TRUE) + - ['ckeditor5' => $group] + - array_slice($implementations, $offset, NULL, TRUE); - } -} - /** * Form submission handler for filter format forms. */ diff --git a/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php b/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php index 85ead6ae51dd6ab134247ab3adccc34e2869de3f..0f0b937f30b531fc8d799dafea8edfa6c7ab2fa5 100644 --- a/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php +++ b/core/modules/ckeditor5/src/Hook/Ckeditor5Hooks.php @@ -2,6 +2,7 @@ namespace Drupal\ckeditor5\Hook; +use Drupal\Core\Hook\OrderAfter; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Asset\AttachedAssetsInterface; use Drupal\Core\Render\Element; @@ -100,8 +101,20 @@ public function theme() : array { /** * Implements hook_form_FORM_ID_alter(). + * + * This module's implementation of form_filter_format_form_alter() must + * happen after the editor module's implementation, as that implementation + * adds the active editor to $form_state. It must also happen after the media + * module's implementation so media_filter_format_edit_form_validate can be + * removed from the validation chain, as that validator is not needed with + * CKEditor 5 and will trigger a false error. */ - #[Hook('form_filter_format_form_alter')] + #[Hook('form_filter_format_form_alter', + order: new OrderAfter( + modules: ['editor', 'media'], + extraTypes: ['form_filter_format_add_form_alter', 'form_filter_format_edit_form_alter'], + ) + )] public function formFilterFormatFormAlter(array &$form, FormStateInterface $form_state, $form_id) : void { $editor = $form_state->get('editor'); // CKEditor 5 plugin config determines the available HTML tags. If an HTML diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index fd038312f285b59a9530d7dc45ce9c63c1094069..ee1ffac87cbaab78220b1603066431a5fa62baf5 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -10,28 +10,6 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageInterface; -/** - * Implements hook_module_implements_alter(). - */ -function content_translation_module_implements_alter(&$implementations, $hook): void { - switch ($hook) { - // Move our hook_entity_type_alter() implementation to the end of the list. - case 'entity_type_alter': - $group = $implementations['content_translation']; - unset($implementations['content_translation']); - $implementations['content_translation'] = $group; - break; - - // Move our hook_entity_bundle_info_alter() implementation to the top of the - // list, so that any other hook implementation can rely on bundles being - // correctly marked as translatable. - case 'entity_bundle_info_alter': - $group = $implementations['content_translation']; - $implementations = ['content_translation' => $group] + $implementations; - break; - } -} - /** * Installs Content Translation's fields for a given entity type. * diff --git a/core/modules/content_translation/src/Hook/ContentTranslationHooks.php b/core/modules/content_translation/src/Hook/ContentTranslationHooks.php index 0f38a7f005dbd98859021152abf987b804531f5c..089b0a5ffba3a9647843e5f0af42fce4d83ebf95 100644 --- a/core/modules/content_translation/src/Hook/ContentTranslationHooks.php +++ b/core/modules/content_translation/src/Hook/ContentTranslationHooks.php @@ -17,6 +17,7 @@ use Drupal\Core\Url; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order; /** * Hook implementations for content_translation. @@ -139,7 +140,7 @@ public function languageTypesInfoAlter(array &$language_types): void { * * @see \Drupal\Core\Entity\Annotation\EntityType */ - #[Hook('entity_type_alter')] + #[Hook('entity_type_alter', order: Order::Last)] public function entityTypeAlter(array &$entity_types) : void { // Provide defaults for translation info. /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */ @@ -221,7 +222,7 @@ public function languageContentSettingsUpdate(ContentLanguageSettingsInterface $ /** * Implements hook_entity_bundle_info_alter(). */ - #[Hook('entity_bundle_info_alter')] + #[Hook('entity_bundle_info_alter', order: Order::First)] public function entityBundleInfoAlter(&$bundles): void { /** @var \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager */ $content_translation_manager = \Drupal::service('content_translation.manager'); diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index 32ce8bee9459aefefe55605965124c5c3ee860aa..37dc492bf7877a84d632a47cad9f82b387a7c647 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -6,20 +6,6 @@ use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; -/** - * Implements hook_module_implements_alter(). - */ -function layout_builder_module_implements_alter(&$implementations, $hook): void { - if ($hook === 'entity_view_alter') { - // Ensure that this module's implementation of hook_entity_view_alter() runs - // last so that other modules that use this hook to render extra fields will - // run before it. - $group = $implementations['layout_builder']; - unset($implementations['layout_builder']); - $implementations['layout_builder'] = $group; - } -} - /** * Implements hook_preprocess_HOOK() for language-content-settings-table.html.twig. */ diff --git a/core/modules/layout_builder/src/Hook/LayoutBuilderHooks.php b/core/modules/layout_builder/src/Hook/LayoutBuilderHooks.php index d12f9f0bc6846a8b60b49359eb6586429106030a..5be94e5e80ee7647bb7ee16aeb0b7ff3a7dc8a85 100644 --- a/core/modules/layout_builder/src/Hook/LayoutBuilderHooks.php +++ b/core/modules/layout_builder/src/Hook/LayoutBuilderHooks.php @@ -26,6 +26,7 @@ use Drupal\Core\Url; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order; /** * Hook implementations for layout_builder. @@ -151,7 +152,7 @@ public function fieldConfigDelete(FieldConfigInterface $field_config): void { * @see \Drupal\layout_builder\Plugin\Block\ExtraFieldBlock::build() * @see layout_builder_module_implements_alter() */ - #[Hook('entity_view_alter')] + #[Hook('entity_view_alter', order: Order::Last)] public function entityViewAlter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display): void { // Only replace extra fields when Layout Builder has been used to alter the // build. See \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple(). diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module b/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module index de2e5316d073037b1f7c5069ef07cbec9102bc37..d7dda399be396db85e6a1ad3425c76dc4bb18656 100644 --- a/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/layout_builder_test.module @@ -49,17 +49,3 @@ function layout_builder_test_preprocess_layout__twocol_section(&$vars): void { ]; } } - -/** - * Implements hook_module_implements_alter(). - */ -function layout_builder_test_module_implements_alter(&$implementations, $hook): void { - if ($hook === 'system_breadcrumb_alter') { - // Move our hook_system_breadcrumb_alter() implementation to run before - // layout_builder_system_breadcrumb_alter(). - $group = $implementations['layout_builder_test']; - $implementations = [ - 'layout_builder_test' => $group, - ] + $implementations; - } -} diff --git a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php index 7a0d4797903557e34306ae6de42bd2166401d4fc..5a2a09729bcd72acff1fa8e72f9a634b360250d9 100644 --- a/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php +++ b/core/modules/layout_builder/tests/modules/layout_builder_test/src/Hook/LayoutBuilderTestHooks.php @@ -11,6 +11,7 @@ use Drupal\Core\Entity\Display\EntityFormDisplayInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\OrderBefore; /** * Hook implementations for layout_builder_test. @@ -115,7 +116,12 @@ public function layoutBuilderEntityFormDisplayAlter(EntityFormDisplayInterface $ /** * Implements hook_system_breadcrumb_alter(). */ - #[Hook('system_breadcrumb_alter')] + #[Hook( + 'system_breadcrumb_alter', + order: new OrderBefore( + modules: ['layout_builder'] + ) + )] public function systemBreadcrumbAlter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context): void { $breadcrumb->addLink(Link::fromTextAndUrl('External link', Url::fromUri('http://www.example.com'))); } diff --git a/core/modules/navigation/navigation.module b/core/modules/navigation/navigation.module index dbc11f191e641f276c8f4d2a309f786b90542ee9..570f13ba3e4a570c626b8a25159772dc70071bbe 100644 --- a/core/modules/navigation/navigation.module +++ b/core/modules/navigation/navigation.module @@ -6,21 +6,6 @@ use Drupal\navigation\TopBarRegion; -/** - * Implements hook_module_implements_alter(). - */ -function navigation_module_implements_alter(&$implementations, $hook): void { - if ($hook == 'page_top') { - $group = $implementations['navigation']; - unset($implementations['navigation']); - $implementations['navigation'] = $group; - } - if ($hook == 'help') { - // We take over the layout_builder hook_help(). - unset($implementations['layout_builder']); - } -} - /** * Prepares variables for navigation top bar template. * diff --git a/core/modules/navigation/src/Hook/NavigationHooks.php b/core/modules/navigation/src/Hook/NavigationHooks.php index 11bf1773dfe365b9b410c9995bc5adea6a8ae1a0..da19ec2dc768f2b6b41d9186a6040e983b81e452 100644 --- a/core/modules/navigation/src/Hook/NavigationHooks.php +++ b/core/modules/navigation/src/Hook/NavigationHooks.php @@ -7,9 +7,12 @@ use Drupal\Core\Config\Action\ConfigActionManager; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Attribute\RemoveHook; +use Drupal\Core\Hook\Order; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\layout_builder\Hook\LayoutBuilderHooks; use Drupal\navigation\NavigationContentLinks; use Drupal\navigation\NavigationRenderer; use Drupal\navigation\Plugin\SectionStorage\NavigationSectionStorage; @@ -55,6 +58,7 @@ public function __construct( * Implements hook_help(). */ #[Hook('help')] + #[RemoveHook('help', class: LayoutBuilderHooks::class, method: 'help')] public function help($route_name, RouteMatchInterface $route_match): ?string { switch ($route_name) { case 'help.page.navigation': @@ -79,7 +83,7 @@ public function help($route_name, RouteMatchInterface $route_match): ?string { /** * Implements hook_page_top(). */ - #[Hook('page_top')] + #[Hook('page_top', order: Order::Last)] public function pageTop(array &$page_top): void { if (!$this->currentUser->hasPermission('access navigation')) { return; diff --git a/core/modules/system/tests/modules/common_test/common_test.module b/core/modules/system/tests/modules/common_test/common_test.module index 7eaeca68fb66ec5ef82013cc20ea995bd4460f75..cc4d26756d1d399bd42cb71fb7f6a3062ff6a5f8 100644 --- a/core/modules/system/tests/modules/common_test/common_test.module +++ b/core/modules/system/tests/modules/common_test/common_test.module @@ -41,24 +41,6 @@ function olivero_drupal_alter_alter(&$data, &$arg2 = NULL, &$arg3 = NULL): void } } -/** - * Implements hook_module_implements_alter(). - * - * @see block_drupal_alter_foo_alter() - */ -function common_test_module_implements_alter(&$implementations, $hook): void { - // For - // \Drupal::moduleHandler()->alter(['drupal_alter', 'drupal_alter_foo'], ...), - // make the block module implementations run after all the other modules. Note - // that when \Drupal::moduleHandler->alter() is called with an array of types, - // the first type is considered primary and controls the module order. - if ($hook == 'drupal_alter_alter' && isset($implementations['block'])) { - $group = $implementations['block']; - unset($implementations['block']); - $implementations['block'] = $group; - } -} - /** * Implements MODULE_preprocess(). * diff --git a/core/modules/system/tests/modules/hook_order_first_alphabetically/hook_order_first_alphabetically.info.yml b/core/modules/system/tests/modules/hook_order_first_alphabetically/hook_order_first_alphabetically.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..bd8dc0b2f73d53bcd3aa582e9fe718cee52c7b60 --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_first_alphabetically/hook_order_first_alphabetically.info.yml @@ -0,0 +1,7 @@ +name: first alphabetically +type: module +description: 'Test module used to test hook ordering.' +package: Testing +version: VERSION +core_version_requirement: '*' +hidden: true diff --git a/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookAfter.php b/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookAfter.php new file mode 100644 index 0000000000000000000000000000000000000000..9dd41b0cbf06cffbc64facbbf5d6fa12d9ef6908 --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookAfter.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_order_first_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\OrderAfter; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookAfter { + + /** + * This pair tests OrderAfter. + */ + #[Hook('custom_hook_test_hook_after', order: new OrderAfter(['hook_order_last_alphabetically']))] + public static function hookAfter(): void { + $GLOBALS['HookAfter'] = 'HookAfter'; + } + +} diff --git a/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookBefore.php b/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookBefore.php new file mode 100644 index 0000000000000000000000000000000000000000..3b7ce8df15acdeecc338f242c8d7a672d90ed881 --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookBefore.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_order_first_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookBefore { + + /** + * This pair tests OrderBefore. + */ + #[Hook('custom_hook_test_hook_before')] + public static function hookBefore(): void { + // This should be run after so HookBefore should not be set. + if (!isset($GLOBALS['HookBefore'])) { + $GLOBALS['HookOutOfOrderTestingHookBefore'] = 'HookOutOfOrderTestingHookBefore'; + } + $GLOBALS['HookRanTestingHookBefore'] = 'HookRanTestingHookBefore'; + } + +} diff --git a/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookFirst.php b/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookFirst.php new file mode 100644 index 0000000000000000000000000000000000000000..31841f75549ab188bf23dead916fbe646e64832f --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookFirst.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_order_first_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookFirst { + + /** + * This pair tests OrderFirst. + */ + #[Hook('custom_hook_test_hook_first')] + public static function hookFirst(): void { + // This should be run after so HookFirst should not be set. + if (!isset($GLOBALS['HookFirst'])) { + $GLOBALS['HookOutOfOrderTestingHookFirst'] = 'HookOutOfOrderTestingHookFirst'; + } + $GLOBALS['HookRanTestingHookFirst'] = 'HookRanTestingHookFirst'; + } + +} diff --git a/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookLast.php b/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookLast.php new file mode 100644 index 0000000000000000000000000000000000000000..b93fd5155a62616621be922a24c6f2326ff887cd --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookLast.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_order_first_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookLast { + + /** + * This pair tests OrderLast. + */ + #[Hook('custom_hook_test_hook_last', order: Order::Last)] + public static function hookLast(): void { + $GLOBALS['HookLast'] = 'HookLast'; + } + +} diff --git a/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookOrderExtraTypes.php b/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookOrderExtraTypes.php new file mode 100644 index 0000000000000000000000000000000000000000..321cc19e5bf1028c111018f7ad31a7f7d28efd83 --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookOrderExtraTypes.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_order_first_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\OrderAfter; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookOrderExtraTypes { + + /** + * This pair tests OrderAfter with ExtraTypes. + */ + #[Hook('custom_hook_extra_types1_alter', + order: new OrderAfter( + modules: ['hook_order_last_alphabetically'], + extraTypes: ['custom_hook_extra_types2_alter'], + ) + )] + public static function customHookExtraTypes(): void { + // This should be run after so HookOrderExtraTypes should not be set. + if (!isset($GLOBALS['HookOrderExtraTypes'])) { + $GLOBALS['HookOutOfOrderTestingOrderExtraTypes'] = 'HookOutOfOrderTestingOrderExtraTypes'; + } + $GLOBALS['HookRanTestingOrderExtraTypes'] = 'HookRanTestingOrderExtraTypes'; + } + +} diff --git a/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookReOrderHookFirst.php b/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookReOrderHookFirst.php new file mode 100644 index 0000000000000000000000000000000000000000..8e48435ebae15e7157bb5d4c4cbde974a3a4e591 --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_first_alphabetically/src/Hook/TestHookReOrderHookFirst.php @@ -0,0 +1,45 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_order_first_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\OrderAfter; +use Drupal\Core\Hook\Attribute\ReOrderHook; +use Drupal\hook_order_last_alphabetically\Hook\TestHookReOrderHookLast; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookReOrderHookFirst { + + /** + * This pair tests ReOrderHook. + */ + #[Hook('custom_hook_override')] + #[ReOrderHook( + 'custom_hook_override', + class: TestHookReOrderHookLast::class, + method: 'customHookOverride', + order: new OrderAfter( + classesAndMethods: [[TestHookReOrderHookFirst::class, 'customHookOverride']], + ) + )] + public static function customHookOverride(): void { + // This normally would run first. + // We override that order in hook_order_second_alphabetically. + // We override, that order here with ReOrderHook. + $GLOBALS['HookRanTestingReOrderHookFirstAlpha'] = 'HookRanTestingReOrderHookFirstAlpha'; + } + +} diff --git a/core/modules/system/tests/modules/hook_order_last_alphabetically/hook_order_last_alphabetically.info.yml b/core/modules/system/tests/modules/hook_order_last_alphabetically/hook_order_last_alphabetically.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..aa08f259dc2e7ef063428315292bb69c00fb8714 --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_last_alphabetically/hook_order_last_alphabetically.info.yml @@ -0,0 +1,7 @@ +name: Hook ordering last +type: module +description: 'Test module used to test hook ordering.' +package: Testing +version: VERSION +core_version_requirement: '*' +hidden: true diff --git a/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookAfter.php b/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookAfter.php new file mode 100644 index 0000000000000000000000000000000000000000..c132f940c319072103f5f89f84d2af1a64ada9e6 --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookAfter.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_order_last_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookAfter { + + /** + * This pair tests OrderAfter. + */ + #[Hook('custom_hook_test_hook_after')] + public static function hookAfter(): void { + // This should be run before so HookAfter should not be set. + if (isset($GLOBALS['HookAfter'])) { + $GLOBALS['HookOutOfOrderTestingHookAfter'] = 'HookOutOfOrderTestingHookAfter'; + } + $GLOBALS['HookRanTestingHookAfter'] = 'HookRanTestingHookAfter'; + } + +} diff --git a/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookBefore.php b/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookBefore.php new file mode 100644 index 0000000000000000000000000000000000000000..03b19b10ffe971ba291cce9af3582f9436008f22 --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookBefore.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_order_last_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\OrderBefore; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookBefore { + + /** + * This pair tests OrderBefore. + */ + #[Hook('custom_hook_test_hook_before', order: new OrderBefore(['hook_order_first_alphabetically']))] + public static function cacheFlush(): void { + $GLOBALS['HookBefore'] = 'HookBefore'; + } + +} diff --git a/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookFirst.php b/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookFirst.php new file mode 100644 index 0000000000000000000000000000000000000000..ee659a32585f10580c6d01b35f4cbbf847137eb1 --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookFirst.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_order_last_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookFirst { + + /** + * This pair tests OrderFirst. + */ + #[Hook('custom_hook_test_hook_first', order: Order::First)] + public static function hookFirst(): void { + $GLOBALS['HookFirst'] = 'HookFirst'; + } + +} diff --git a/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookLast.php b/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookLast.php new file mode 100644 index 0000000000000000000000000000000000000000..36b32f08678bd9a53c3d867690a8f3d586cedd2a --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookLast.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_order_last_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookLast { + + /** + * This pair tests OrderLast. + */ + #[Hook('custom_hook_test_hook_last')] + public static function hookLast(): void { + // This should be run before so HookLast should not be set. + if (isset($GLOBALS['HookLast'])) { + $GLOBALS['HookOutOfOrderTestingHookLast'] = 'HookOutOfOrderTestingHookLast'; + } + $GLOBALS['HookRanTestingHookLast'] = 'HookRanTestingHookLast'; + } + +} diff --git a/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookOrderExtraTypes.php b/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookOrderExtraTypes.php new file mode 100644 index 0000000000000000000000000000000000000000..2ca8a0d93f42eb0cd61dd670ca13ab113361f131 --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookOrderExtraTypes.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_order_last_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookOrderExtraTypes { + + /** + * This pair tests OrderAfter with ExtraTypes. + */ + #[Hook('custom_hook_extra_types2_alter')] + public static function customHookExtraTypes(): void { + $GLOBALS['HookOrderExtraTypes'] = 'HookOrderExtraTypes'; + } + +} diff --git a/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookReOrderHookLast.php b/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookReOrderHookLast.php new file mode 100644 index 0000000000000000000000000000000000000000..a46e3583609ec1191e41cde04bf716b8c8342769 --- /dev/null +++ b/core/modules/system/tests/modules/hook_order_last_alphabetically/src/Hook/TestHookReOrderHookLast.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_order_last_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookReOrderHookLast { + + /** + * This pair tests ReOrderHook. + */ + #[Hook('custom_hook_override', order: Order::First)] + public static function customHookOverride(): void { + // This normally would run second. + // We override that order here with Order::First. + // We override, that order in hook_order_first_alphabetically with + // ReOrderHook. + if (!isset($GLOBALS['HookRanTestingReOrderHookFirstAlpha'])) { + $GLOBALS['HookOutOfOrderTestingReOrderHook'] = 'HookOutOfOrderTestingReOrderHook'; + } + $GLOBALS['HookRanTestingReOrderHookLastAlpha'] = 'HookRanTestingReOrderHookLastAlpha'; + } + +} diff --git a/core/modules/system/tests/modules/hook_second_order_first_alphabetically/hook_second_order_first_alphabetically.info.yml b/core/modules/system/tests/modules/hook_second_order_first_alphabetically/hook_second_order_first_alphabetically.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..bd8dc0b2f73d53bcd3aa582e9fe718cee52c7b60 --- /dev/null +++ b/core/modules/system/tests/modules/hook_second_order_first_alphabetically/hook_second_order_first_alphabetically.info.yml @@ -0,0 +1,7 @@ +name: first alphabetically +type: module +description: 'Test module used to test hook ordering.' +package: Testing +version: VERSION +core_version_requirement: '*' +hidden: true diff --git a/core/modules/system/tests/modules/hook_second_order_first_alphabetically/src/Hook/TestHookAfterClassMethod.php b/core/modules/system/tests/modules/hook_second_order_first_alphabetically/src/Hook/TestHookAfterClassMethod.php new file mode 100644 index 0000000000000000000000000000000000000000..b373391735782959a5404f5a2925e135b387dd24 --- /dev/null +++ b/core/modules/system/tests/modules/hook_second_order_first_alphabetically/src/Hook/TestHookAfterClassMethod.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_second_order_first_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\OrderAfter; +use Drupal\hook_second_order_last_alphabetically\Hook\TestHookAfterClassMethod as TestHookAfterClassMethodForAfter; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookAfterClassMethod { + + /** + * This pair tests OrderAfter with a passed class and method. + */ + #[Hook('custom_hook_test_hook_after_class_method', + order: new OrderAfter( + classesAndMethods: [[TestHookAfterClassMethodForAfter::class, 'hookAfterClassMethod']], + ) + )] + public static function hookAfterClassMethod(): void { + $GLOBALS['HookAfterClassMethod'] = 'HookAfterMethod'; + } + +} diff --git a/core/modules/system/tests/modules/hook_second_order_last_alphabetically/hook_second_order_last_alphabetically.info.yml b/core/modules/system/tests/modules/hook_second_order_last_alphabetically/hook_second_order_last_alphabetically.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..aa08f259dc2e7ef063428315292bb69c00fb8714 --- /dev/null +++ b/core/modules/system/tests/modules/hook_second_order_last_alphabetically/hook_second_order_last_alphabetically.info.yml @@ -0,0 +1,7 @@ +name: Hook ordering last +type: module +description: 'Test module used to test hook ordering.' +package: Testing +version: VERSION +core_version_requirement: '*' +hidden: true diff --git a/core/modules/system/tests/modules/hook_second_order_last_alphabetically/src/Hook/TestHookAfterClassMethod.php b/core/modules/system/tests/modules/hook_second_order_last_alphabetically/src/Hook/TestHookAfterClassMethod.php new file mode 100644 index 0000000000000000000000000000000000000000..d69aaa546fb0acd3a4cd5930a93757821b58b19a --- /dev/null +++ b/core/modules/system/tests/modules/hook_second_order_last_alphabetically/src/Hook/TestHookAfterClassMethod.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_second_order_last_alphabetically\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for verifying ordering hooks by attributes. + * + * We must ensure that the order of the modules is expected and then change + * the order that the hooks are run in order to verify. This module + * comes in a pair first alphabetically and last alphabetically. + * + * In the normal order a hook implemented by first alphabetically would run + * before the same hook in last alphabetically. + * + * Each method pair tests one hook ordering permutation. + */ +class TestHookAfterClassMethod { + + /** + * This pair tests #[HookAfter]. + */ + #[Hook('custom_hook_test_hook_after_class_method')] + public static function hookAfterClassMethod(): void { + // This should be run before so HookAfter should not be set. + if (isset($GLOBALS['HookAfterClassMethod'])) { + $GLOBALS['HookOutOfOrderTestingHookAfterClassMethod'] = 'HookOutOfOrderTestingHookAfterClassMethod'; + } + $GLOBALS['HookRanTestingHookAfterClassMethod'] = 'HookRanTestingHookAfterClassMethod'; + } + +} diff --git a/core/modules/system/tests/modules/hook_test_remove/hook_test_remove.info.yml b/core/modules/system/tests/modules/hook_test_remove/hook_test_remove.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..9245dd4208f7cc17a6eccfe380218e5c09b4463e --- /dev/null +++ b/core/modules/system/tests/modules/hook_test_remove/hook_test_remove.info.yml @@ -0,0 +1,7 @@ +name: Hook test removal +type: module +description: 'Test module used to test hook removal.' +package: Testing +version: VERSION +core_version_requirement: '*' +hidden: true diff --git a/core/modules/system/tests/modules/hook_test_remove/src/Hook/TestHookRemove.php b/core/modules/system/tests/modules/hook_test_remove/src/Hook/TestHookRemove.php new file mode 100644 index 0000000000000000000000000000000000000000..3ea922ec36ce618149c0358758daa0f7bb8358e0 --- /dev/null +++ b/core/modules/system/tests/modules/hook_test_remove/src/Hook/TestHookRemove.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\hook_test_remove\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Attribute\RemoveHook; + +/** + * Add a hook here, then remove it with another attribute. + */ +class TestHookRemove { + + /** + * This hook should not be run because the next hook replaces it. + */ + #[Hook('custom_hook1')] + public static function hookDoNotRun(): void { + $GLOBALS['HookShouldNotRunTestRemove'] = 'HookShouldNotRunTestRemove'; + } + + /** + * This hook should run and prevent custom_hook1. + */ + #[Hook('custom_hook2')] + #[RemoveHook( + 'custom_hook1', + class: TestHookRemove::class, + method: 'hookDoNotRun' + )] + public static function hookDoRun(): void { + $GLOBALS['HookShouldRunTestRemove'] = 'HookShouldRunTestRemove'; + } + +} diff --git a/core/modules/system/tests/modules/module_implements_alter_test/module_implements_alter_test.implementations.inc b/core/modules/system/tests/modules/module_implements_alter_test/module_implements_alter_test.implementations.inc new file mode 100644 index 0000000000000000000000000000000000000000..7b6bac4ae95dc2d23564c4b807dfa352a3e263be --- /dev/null +++ b/core/modules/system/tests/modules/module_implements_alter_test/module_implements_alter_test.implementations.inc @@ -0,0 +1,17 @@ +<?php + +/** + * @file + * Include file for test module. + */ + +declare(strict_types=1); + +/** + * Implements hook_altered_test_hook(). + * + * @see module_implements_alter_test_module_implements_alter() + */ +function module_implements_alter_test_altered_test_hook(): string { + return __FUNCTION__; +} diff --git a/core/modules/system/tests/modules/module_implements_alter_test/module_implements_alter_test.info.yml b/core/modules/system/tests/modules/module_implements_alter_test/module_implements_alter_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..25995b17cd9aac88bd987ec1e491499e20fa0f65 --- /dev/null +++ b/core/modules/system/tests/modules/module_implements_alter_test/module_implements_alter_test.info.yml @@ -0,0 +1,6 @@ +name: 'Test hook_module_implements_alter' +type: module +description: 'Support module for module system testing.' +package: Testing +version: VERSION +core_version_requirement: '*' diff --git a/core/modules/system/tests/modules/module_implements_alter_test/module_implements_alter_test.module b/core/modules/system/tests/modules/module_implements_alter_test/module_implements_alter_test.module new file mode 100644 index 0000000000000000000000000000000000000000..6e7452988600873488d1a5da7cf5320150586f92 --- /dev/null +++ b/core/modules/system/tests/modules/module_implements_alter_test/module_implements_alter_test.module @@ -0,0 +1,42 @@ +<?php + +/** + * @file + * Module file for test module. + */ + +declare(strict_types=1); + +function test_auto_include(): void {} + +/** + * Implements hook_module_implements_alter(). + * + * @see \Drupal\system\Tests\Module\ModuleImplementsAlterTest::testModuleImplementsAlter() + * @see module_implements_alter_test_module_implements_alter() + */ +function module_implements_alter_test_module_implements_alter(&$implementations, $hook): void { + if ($hook === 'altered_test_hook') { + // Add a hook implementation, that will be found in + // module_implements_alter_test.implementation.inc. + $implementations['module_implements_alter_test'] = 'implementations'; + } + if ($hook === 'unimplemented_test_hook') { + // Add the non-existing function module_implements_alter_test_unimplemented_test_hook(). This + // should cause an exception to be thrown in + // \Drupal\Core\Extension\ModuleHandler::buildImplementationInfo('unimplemented_test_hook'). + $implementations['module_implements_alter_test'] = FALSE; + } + + // For + // \Drupal::moduleHandler()->alter(['drupal_alter', 'drupal_alter_foo'], ...), + // make the block module implementations run after all the other modules. Note + // that when \Drupal::moduleHandler->alter() is called with an array of types, + // the first type is considered primary and controls the module order. + if ($hook == 'drupal_alter_alter' && isset($implementations['block'])) { + $group = $implementations['block']; + unset($implementations['block']); + $implementations['block'] = $group; + } + +} diff --git a/core/modules/system/tests/modules/module_test/module_test.module b/core/modules/system/tests/modules/module_test/module_test.module index b7f320a35e3c26b8f704cf593cad67d66e640ecf..35561f09c8a0c0dde33fea382604af5029ea9d90 100644 --- a/core/modules/system/tests/modules/module_test/module_test.module +++ b/core/modules/system/tests/modules/module_test/module_test.module @@ -95,23 +95,3 @@ function module_test_modules_uninstalled($modules): void { // can check that the modules were uninstalled in the correct sequence. \Drupal::state()->set('module_test.uninstall_order', $modules); } - -/** - * Implements hook_module_implements_alter(). - * - * @see module_test_altered_test_hook() - * @see \Drupal\system\Tests\Module\ModuleImplementsAlterTest::testModuleImplementsAlter() - */ -function module_test_module_implements_alter(&$implementations, $hook): void { - if ($hook === 'altered_test_hook') { - // Add a hook implementation, that will be found in - // module_test.implementation.inc. - $implementations['module_test'] = 'implementations'; - } - if ($hook === 'unimplemented_test_hook') { - // Add the non-existing function module_test_unimplemented_test_hook(). This - // should cause an exception to be thrown in - // \Drupal\Core\Extension\ModuleHandler::buildImplementationInfo('unimplemented_test_hook'). - $implementations['module_test'] = FALSE; - } -} diff --git a/core/modules/system/tests/src/Kernel/Common/AlterTest.php b/core/modules/system/tests/src/Kernel/Common/AlterTest.php index 18217579ad952cc5691eb18bd87354a8d69507bc..0ff2115214f4564c8d1870e165c01bc9f968f70e 100644 --- a/core/modules/system/tests/src/Kernel/Common/AlterTest.php +++ b/core/modules/system/tests/src/Kernel/Common/AlterTest.php @@ -19,11 +19,14 @@ class AlterTest extends KernelTestBase { protected static $modules = [ 'block', 'common_test', + 'module_implements_alter_test', 'system', ]; /** * Tests if the theme has been altered. + * + * @group legacy */ public function testDrupalAlter(): void { // This test depends on Olivero, so make sure that it is always the current diff --git a/core/modules/user/tests/modules/user_hooks_test/user_hooks_test.info.yml b/core/modules/user/tests/modules/user_hooks_test/user_hooks_test.info.yml old mode 100644 new mode 100755 diff --git a/core/modules/workspaces/src/Hook/EntityOperations.php b/core/modules/workspaces/src/Hook/EntityOperations.php index c459f4d065e3fbd47998ffa6941a561452993034..915499f685eb9fc56c9254edaa157f468de26650 100644 --- a/core/modules/workspaces/src/Hook/EntityOperations.php +++ b/core/modules/workspaces/src/Hook/EntityOperations.php @@ -12,6 +12,10 @@ use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Attribute\ReOrderHook; +use Drupal\Core\Hook\Order; +use Drupal\Core\Hook\OrderBefore; +use Drupal\content_moderation\Hook\ContentModerationHooks; use Drupal\workspaces\WorkspaceAssociationInterface; use Drupal\workspaces\WorkspaceInformationInterface; use Drupal\workspaces\WorkspaceManagerInterface; @@ -72,7 +76,12 @@ public function entityPreload(array $ids, string $entity_type_id): array { /** * Implements hook_entity_presave(). */ - #[Hook('entity_presave')] + #[Hook('entity_presave', order: Order::First)] + #[ReOrderHook('entity_presave', + class: ContentModerationHooks::class, + method: 'entityPresave', + order: new OrderBefore(['workspaces']) + )] public function entityPresave(EntityInterface $entity): void { if ($this->shouldSkipOperations($entity)) { return; @@ -129,7 +138,7 @@ public function entityPresave(EntityInterface $entity): void { /** * Implements hook_entity_insert(). */ - #[Hook('entity_insert')] + #[Hook('entity_insert', order: Order::Last)] public function entityInsert(EntityInterface $entity): void { if ($entity->getEntityTypeId() === 'workspace') { $this->workspaceAssociation->workspaceInsert($entity); diff --git a/core/modules/workspaces/workspaces.module b/core/modules/workspaces/workspaces.module deleted file mode 100644 index a053105f20c372f667e504ce4560b29fde55be14..0000000000000000000000000000000000000000 --- a/core/modules/workspaces/workspaces.module +++ /dev/null @@ -1,35 +0,0 @@ -<?php - -/** - * @file - */ - -/** - * Implements hook_module_implements_alter(). - */ -function workspaces_module_implements_alter(&$implementations, $hook): void { - // Move our 'hook_entity_presave' implementation at the beginning to ensure - // that other presave implementations are aware of the changes done in - // \Drupal\workspaces\Hook\EntityOperations::entityPresave(). - if ($hook === 'entity_presave') { - $implementation = $implementations['workspaces']; - $implementations = ['workspaces' => $implementation] + $implementations; - - // Move Content Moderation's implementation before Workspaces, so we can - // alter the publishing status for the default revision. - if (isset($implementations['content_moderation'])) { - $implementation = $implementations['content_moderation']; - $implementations = ['content_moderation' => $implementation] + $implementations; - } - } - - // Move our 'hook_entity_insert' implementation at the end to ensure that - // the second (pending) revision created for published entities is not used - // by other 'hook_entity_insert' implementations. - // @see \Drupal\workspaces\Hook\EntityOperations::entityInsert() - if ($hook === 'entity_insert') { - $group = $implementations['workspaces']; - unset($implementations['workspaces']); - $implementations['workspaces'] = $group; - } -} diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ModuleImplementsAlterTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ModuleImplementsAlterTest.php index ef0022b1bfaf52b974e6aed47a2c9dd0f7ecea71..2e9447c49913733b648e7dc9c4799596bbfedbf0 100644 --- a/core/tests/Drupal/KernelTests/Core/Extension/ModuleImplementsAlterTest.php +++ b/core/tests/Drupal/KernelTests/Core/Extension/ModuleImplementsAlterTest.php @@ -10,6 +10,8 @@ * Tests hook_module_implements_alter(). * * @group Module + * + * @group legacy */ class ModuleImplementsAlterTest extends KernelTestBase { @@ -22,7 +24,7 @@ class ModuleImplementsAlterTest extends KernelTestBase { * Tests hook_module_implements_alter() adding an implementation. * * @see \Drupal\Core\Extension\ModuleHandler::buildImplementationInfo() - * @see module_test_module_implements_alter() + * @see module_implements_alter_test_module_implements_alter() */ public function testModuleImplementsAlter(): void { @@ -32,38 +34,33 @@ public function testModuleImplementsAlter(): void { $this->assertSame(\Drupal::moduleHandler(), $module_handler, 'Module handler instance is still the same.'); - // Install the module_test module. - \Drupal::service('module_installer')->install(['module_test']); + // Install the module_implements_alter_test module. + \Drupal::service('module_installer')->install(['module_implements_alter_test']); // Assert that the \Drupal::moduleHandler() instance has been replaced. $this->assertNotSame(\Drupal::moduleHandler(), $module_handler, 'The \Drupal::moduleHandler() instance has been replaced during \Drupal::moduleHandler()->install().'); - // Assert that module_test.module is now included. - $this->assertTrue(function_exists('module_test_modules_installed'), - 'The file module_test.module was successfully included.'); - - $this->assertArrayHasKey('module_test', \Drupal::moduleHandler()->getModuleList()); + // Assert that module_implements_alter_test.module is now included. + $this->assertTrue(function_exists('test_auto_include'), + 'The file module_implements_alter_test.module was successfully included.'); - $this->assertTrue(\Drupal::moduleHandler()->hasImplementations('modules_installed', 'module_test'), - 'module_test implements hook_modules_installed().'); + $this->assertTrue(\Drupal::moduleHandler()->hasImplementations('module_implements_alter', 'module_implements_alter_test'), + 'module_implements_alter_test implements hook_module_implements_alter().'); - $this->assertTrue(\Drupal::moduleHandler()->hasImplementations('module_implements_alter', 'module_test'), - 'module_test implements hook_module_implements_alter().'); - - // Assert that module_test.implementations.inc is not included yet. - $this->assertFalse(function_exists('module_test_altered_test_hook'), - 'The file module_test.implementations.inc is not included yet.'); + // Assert that module_implements_alter_test.implementations.inc is not included yet. + $this->assertFalse(function_exists('module_implements_alter_test_altered_test_hook'), + 'The file module_implements_alter_test.implementations.inc is not included yet.'); // Trigger hook discovery for hook_altered_test_hook(). - // Assert that module_test_module_implements_alter(*, 'altered_test_hook') + // Assert that module_implements_alter_test_module_implements_alter(*, 'altered_test_hook') // has added an implementation. - $this->assertTrue(\Drupal::moduleHandler()->hasImplementations('altered_test_hook', 'module_test'), - 'module_test implements hook_altered_test_hook().'); + $this->assertTrue(\Drupal::moduleHandler()->hasImplementations('altered_test_hook', 'module_implements_alter_test'), + 'module_implements_alter_test implements hook_altered_test_hook().'); + + // Assert that module_implements_alter_test.implementations.inc was included as part of the process. + $this->assertTrue(function_exists('module_implements_alter_test_altered_test_hook'), + 'The file module_implements_alter_test.implementations.inc was included.'); - // Assert that module_test.implementations.inc was included as part of the - // process. - $this->assertTrue(function_exists('module_test_altered_test_hook'), - 'The file module_test.implementations.inc was included.'); } } diff --git a/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php b/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php index 4156481d3b924a88d8fcf1b0953b80e1669b66c2..10765f3a084b2f4ae501384ad9b765e22aa821de 100644 --- a/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php +++ b/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php @@ -52,6 +52,8 @@ public function testSymlink(): void { /** * Test that ordering works. + * + * @group legacy */ public function testOrdering(): void { $container = new ContainerBuilder(); @@ -82,6 +84,23 @@ public function testOrdering(): void { $this->assertLessThan($priorities['drupal_hook.order2']['order'], $priorities['drupal_hook.order2']['module_handler_test_all2_order2']); } + /** + * Test LegacyModuleImplementsAlter. + */ + public function testLegacyModuleImplementsAlter(): void { + $container = new ContainerBuilder(); + $module_filenames = [ + 'module_implements_alter_test_legacy' => ['pathname' => "core/tests/Drupal/Tests/Core/Extension/modules/module_implements_alter_test_legacy/module_implements_alter_test_legacy.info.yml"], + ]; + include_once 'core/tests/Drupal/Tests/Core/Extension/modules/module_implements_alter_test_legacy/module_implements_alter_test_legacy.module'; + $container->setParameter('container.modules', $module_filenames); + $container->setDefinition('module_handler', new Definition()); + (new HookCollectorPass())->process($container); + + // This test will also fail if the deprecation notice shows up. + $this->assertFalse(isset($GLOBALS['ShouldNotRunLegacyModuleImplementsAlter'])); + } + /** * Test hooks implemented on behalf of an uninstalled module. * @@ -121,7 +140,6 @@ public function testProceduralHooksSkippedWhenConfigured(): void { $this->assertFalse(isset($GLOBALS['procedural_attribute_skip_after_attribute'])); $this->assertTrue(isset($GLOBALS['procedural_attribute_skip_find'])); $this->assertTrue(isset($GLOBALS['skipped_procedural_oop_cache_flush'])); - } /** @@ -137,4 +155,151 @@ public function testHookAttribute(): void { $this->assertTrue(isset($GLOBALS['hook_invoke_method'])); } + /** + * Tests hook ordering with attributes. + */ + public function testHookFirst(): void { + $module_installer = $this->container->get('module_installer'); + $this->assertTrue($module_installer->install(['hook_order_first_alphabetically'])); + $this->assertTrue($module_installer->install(['hook_order_last_alphabetically'])); + $this->assertFalse(isset($GLOBALS['HookFirst'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingHookFirst'])); + $this->assertFalse(isset($GLOBALS['HookRanTestingHookFirst'])); + $module_handler = $this->container->get('module_handler'); + $data = ['hi']; + $module_handler->invokeAll('custom_hook_test_hook_first', $data); + $this->assertTrue(isset($GLOBALS['HookFirst'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingHookFirst'])); + $this->assertTrue(isset($GLOBALS['HookRanTestingHookFirst'])); + } + + /** + * Tests hook ordering with attributes. + */ + public function testHookAfter(): void { + $module_installer = $this->container->get('module_installer'); + $this->assertTrue($module_installer->install(['hook_order_first_alphabetically'])); + $this->assertTrue($module_installer->install(['hook_order_last_alphabetically'])); + $this->assertFalse(isset($GLOBALS['HookAfter'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingHookAfter'])); + $this->assertFalse(isset($GLOBALS['HookRanTestingHookAfter'])); + $module_handler = $this->container->get('module_handler'); + $data = ['hi']; + $module_handler->invokeAll('custom_hook_test_hook_after', $data); + $this->assertTrue(isset($GLOBALS['HookAfter'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingHookAfter'])); + $this->assertTrue(isset($GLOBALS['HookRanTestingHookAfter'])); + } + + /** + * Tests hook ordering with attributes. + */ + public function testHookAfterClassMethod(): void { + $module_installer = $this->container->get('module_installer'); + $this->assertTrue($module_installer->install(['hook_second_order_first_alphabetically'])); + $this->assertTrue($module_installer->install(['hook_second_order_last_alphabetically'])); + $this->assertFalse(isset($GLOBALS['HookAfterClassMethod'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingHookAfterClassMethod'])); + $this->assertFalse(isset($GLOBALS['HookRanTestingHookAfterClassMethod'])); + $module_handler = $this->container->get('module_handler'); + $data = ['hi']; + $module_handler->invokeAll('custom_hook_test_hook_after_class_method', $data); + $this->assertTrue(isset($GLOBALS['HookAfterClassMethod'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingHookAfterClassMethod'])); + $this->assertTrue(isset($GLOBALS['HookRanTestingHookAfterClassMethod'])); + } + + /** + * Tests hook ordering with attributes. + */ + public function testHookBefore(): void { + $module_installer = $this->container->get('module_installer'); + $this->assertTrue($module_installer->install(['hook_order_first_alphabetically'])); + $this->assertTrue($module_installer->install(['hook_order_last_alphabetically'])); + $this->assertFalse(isset($GLOBALS['HookBefore'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingHookBefore'])); + $this->assertFalse(isset($GLOBALS['HookRanTestingHookBefore'])); + $module_handler = $this->container->get('module_handler'); + $data = ['hi']; + $module_handler->invokeAll('custom_hook_test_hook_before', $data); + $this->assertTrue(isset($GLOBALS['HookBefore'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingHookBefore'])); + $this->assertTrue(isset($GLOBALS['HookRanTestingHookBefore'])); + } + + /** + * Tests hook ordering with attributes. + */ + public function testHookOrderExtraTypes(): void { + $module_installer = $this->container->get('module_installer'); + $this->assertTrue($module_installer->install(['hook_order_first_alphabetically'])); + $this->assertTrue($module_installer->install(['hook_order_last_alphabetically'])); + $this->assertFalse(isset($GLOBALS['HookOrderExtraTypes'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingOrderExtraTypes'])); + $this->assertFalse(isset($GLOBALS['HookRanTestingOrderExtraTypes'])); + $module_handler = $this->container->get('module_handler'); + $hooks = [ + 'custom_hook', + 'custom_hook_extra_types1', + 'custom_hook_extra_types2', + ]; + $data = ['hi']; + $module_handler->alter($hooks, $data); + $this->assertTrue(isset($GLOBALS['HookOrderExtraTypes'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingOrderExtraTypes'])); + $this->assertTrue(isset($GLOBALS['HookRanTestingOrderExtraTypes'])); + } + + /** + * Tests hook ordering with attributes. + */ + public function testHookLast(): void { + $module_installer = $this->container->get('module_installer'); + $this->assertTrue($module_installer->install(['hook_order_first_alphabetically'])); + $this->assertTrue($module_installer->install(['hook_order_last_alphabetically'])); + $this->assertFalse(isset($GLOBALS['HookLast'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingHookLast'])); + $this->assertFalse(isset($GLOBALS['HookRanTestingHookLast'])); + $module_handler = $this->container->get('module_handler'); + $data = ['hi']; + $module_handler->invokeAll('custom_hook_test_hook_last', $data); + $this->assertTrue(isset($GLOBALS['HookLast'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingHookLast'])); + $this->assertTrue(isset($GLOBALS['HookRanTestingHookLast'])); + } + + /** + * Tests hook remove. + */ + public function testHookRemove(): void { + $module_installer = $this->container->get('module_installer'); + $this->assertTrue($module_installer->install(['hook_test_remove'])); + $this->assertFalse(isset($GLOBALS['HookShouldRunTestRemove'])); + $this->assertFalse(isset($GLOBALS['HookShouldNotRunTestRemove'])); + $module_handler = $this->container->get('module_handler'); + $data = ['hi']; + $module_handler->invokeAll('custom_hook1', $data); + $module_handler->invokeAll('custom_hook2', $data); + $this->assertTrue(isset($GLOBALS['HookShouldRunTestRemove'])); + $this->assertFalse(isset($GLOBALS['HookShouldNotRunTestRemove'])); + } + + /** + * Tests hook override. + */ + public function testHookOverride(): void { + $module_installer = $this->container->get('module_installer'); + $this->assertTrue($module_installer->install(['hook_order_first_alphabetically'])); + $this->assertTrue($module_installer->install(['hook_order_last_alphabetically'])); + $this->assertFalse(isset($GLOBALS['HookRanTestingReOrderHookFirstAlpha'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingReOrderHook'])); + $this->assertFalse(isset($GLOBALS['HookRanTestingReOrderHookLastAlpha'])); + $module_handler = $this->container->get('module_handler'); + $data = ['hi']; + $module_handler->invokeAll('custom_hook_override', $data); + $this->assertTrue(isset($GLOBALS['HookRanTestingReOrderHookFirstAlpha'])); + $this->assertFalse(isset($GLOBALS['HookOutOfOrderTestingReOrderHook'])); + $this->assertTrue(isset($GLOBALS['HookRanTestingReOrderHookLastAlpha'])); + } + } diff --git a/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerTest.php b/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerTest.php index 500973836edec61d069c9b83d7eda0ae4d5e7740..143c56c542f8ae8a879347b3fbd515e73e78ddac 100644 --- a/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerTest.php @@ -79,8 +79,11 @@ public function testLoadModule(): void { * Tests loading all modules. * * @covers ::loadAll + * + * @group legacy */ public function testLoadAllModules(): void { + $this->expectDeprecation('module_handler_test_all1_module_implements_alter without a #[LegacyModuleImplementsAlter] attribute is deprecated in drupal:11.2.0 and removed in drupal:12.0.0. See https://www.drupal.org/node/3496788'); $module_handler = $this->getModuleHandler(); $module_handler->addModule('module_handler_test_all1', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test_all1'); $module_handler->addModule('module_handler_test_all2', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test_all2'); @@ -326,8 +329,11 @@ public function testImplementsHookModuleEnabled(): void { * Tests invoke all. * * @covers ::invokeAll + * + * @group legacy */ public function testInvokeAll(): void { + $this->expectDeprecation('module_handler_test_all1_module_implements_alter without a #[LegacyModuleImplementsAlter] attribute is deprecated in drupal:11.2.0 and removed in drupal:12.0.0. See https://www.drupal.org/node/3496788'); $module_handler = $this->getModuleHandler(); $module_handler->addModule('module_handler_test_all1', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test_all1'); $module_handler->addModule('module_handler_test_all2', 'core/tests/Drupal/Tests/Core/Extension/modules/module_handler_test_all2'); diff --git a/core/tests/Drupal/Tests/Core/Extension/modules/module_implements_alter_test_legacy/module_implements_alter_test_legacy.info.yml b/core/tests/Drupal/Tests/Core/Extension/modules/module_implements_alter_test_legacy/module_implements_alter_test_legacy.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..e286a4ffac38950580c6377db8e62592b94bf2b5 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Extension/modules/module_implements_alter_test_legacy/module_implements_alter_test_legacy.info.yml @@ -0,0 +1,6 @@ +name: 'Module for testing LegacyModuleImplementsAlter' +type: module +description: 'Support module for module system testing.' +package: Testing +version: VERSION +core_version_requirement: '*' diff --git a/core/tests/Drupal/Tests/Core/Extension/modules/module_implements_alter_test_legacy/module_implements_alter_test_legacy.module b/core/tests/Drupal/Tests/Core/Extension/modules/module_implements_alter_test_legacy/module_implements_alter_test_legacy.module new file mode 100644 index 0000000000000000000000000000000000000000..355b561694973eff8fdbab027339282c20d9dfe1 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Extension/modules/module_implements_alter_test_legacy/module_implements_alter_test_legacy.module @@ -0,0 +1,20 @@ +<?php + +/** + * @file + * Module file for test module. + */ + +declare(strict_types=1); + +use Drupal\Core\Hook\Attribute\LegacyModuleImplementsAlter; + +/** + * Implements hook_module_implements_alter(). + * + * @see \Drupal\system\Tests\Module\ModuleImplementsAlterTest::testModuleImplementsAlter() + */ +#[LegacyModuleImplementsAlter] +function module_implements_alter_test_legacy_module_implements_alter(&$implementations, $hook): void { + $GLOBALS['ShouldNotRunLegacyModuleImplementsAlter'] = TRUE; +} diff --git a/core/tests/Drupal/Tests/Core/Hook/HookCollectorPassTest.php b/core/tests/Drupal/Tests/Core/Hook/HookCollectorPassTest.php index 4b92b8d6d25f4e4217e09f0dea4bb730d716f8fd..2d6fba3b451f19a5516a85996d12bc9d7e2f58a3 100644 --- a/core/tests/Drupal/Tests/Core/Hook/HookCollectorPassTest.php +++ b/core/tests/Drupal/Tests/Core/Hook/HookCollectorPassTest.php @@ -6,7 +6,6 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Extension\ProceduralCall; -use Drupal\Core\Hook\Attribute\Hook; use Drupal\Core\Hook\HookCollectorPass; use Drupal\Tests\UnitTestCase; use Drupal\Tests\Core\GroupIncludesTestTrait; @@ -85,35 +84,4 @@ public function testGroupIncludes(): void { $this->assertSame(self::GROUP_INCLUDES, $argument); } - /** - * @covers ::getHookAttributesInClass - */ - public function testGetHookAttributesInClass(): void { - // @phpstan-ignore-next-line - $getHookAttributesInClass = fn ($class) => $this->getHookAttributesInClass($class); - $p = new HookCollectorPass(); - $getHookAttributesInClass = $getHookAttributesInClass->bindTo($p, $p); - - $x = new class { - - #[Hook('foo')] - function foo(): void {} - - }; - $hooks = $getHookAttributesInClass(get_class($x)); - $hook = reset($hooks); - $this->assertInstanceOf(Hook::class, $hook); - $this->assertSame('foo', $hook->hook); - - $x = new class { - - #[Hook('install')] - function foo(): void {} - - }; - $this->expectException(\LogicException::class); - // This will throw exception, and stop code execution. - $getHookAttributesInClass(get_class($x)); - } - }