diff --git a/core/core.api.php b/core/core.api.php index 0500a3c58aecebc87336db728c2818d51135bc86..e93fa0bb5b5c9c168a55ee726c3415e17b6bf08e 100644 --- a/core/core.api.php +++ b/core/core.api.php @@ -1606,6 +1606,14 @@ * modules that they interact with. Your modules can also define their own * hooks, in order to let other modules interact with them. * + * Hook implementations will execute in the following order. + * order. + * - Module weight. + * - Alphabetical by module name. + * - This order can be modified by using the order parameter on the #[Hook] + * attribute, using the #[ReorderHook] attribute, or implementing the legacy + * hook_module_implements_alter. + * * @section implementing Implementing a hook * * There are two ways to implement a hook: @@ -1657,6 +1665,7 @@ * Legacy meta hooks: * - hook_hook_info() * - hook_module_implements_alter() + * @see \Drupal\Core\Hook\Attribute\LegacyModuleImplementsAlter * * Install hooks: * - hook_install() @@ -1709,6 +1718,90 @@ * @see \Drupal\Core\Hook\Attribute\Hook * @see \Drupal::moduleHandler() * + * @section ordering_hooks Ordering hook implementations + * + * The order in which hook implementations are executed can be modified. A hook + * can be placed first or last in the order of execution. It can also be placed + * before or after the execution of another module's implementation of the same + * hook. When changing the order of execution in relation to a specific module + * either the module name or the class and method can be used. + * + * Use the order argument of the Hook attribute to order the execution of + * hooks. + * + * Example of executing 'entity_type_alter' of my_module first: + * @code + * #[Hook('entity_type_alter', order: Order::First)] + * @endcode + * + * Example of executing 'entity_type_alter' of my_module last: + * @code + * #[Hook('entity_type_alter', order: Order::Last)] + * @endcode + * + * Example of executing 'entity_type_alter' before the execution of the + * implementation in the foo module: + * @code + * #[Hook('entity_type_alter', order: new OrderBefore(['foo']))] + * @endcode + * + * Example of executing 'entity_type_alter' after the execution of the + * implementation in the foo module: + * @code + * #[Hook('entity_type_alter', order: new OrderAfter(['foo']))] + * @endcode + * + * Example of executing 'entity_type_alter' before two methods. One in the Foo + * class and one in the Bar class. + * @code + * #[Hook('entity_type_alter', + * order: new OrderBefore( + * classesAndMethods: [ + * [Foo::class, 'someMethod'], + * [Bar::class, 'someOtherMethod'], + * ] + * ) + * )] + * @endcode + * + * @see \Drupal\Core\Hook\Attribute\Hook + * @see \Drupal\Core\Hook\Order\Order + * @see \Drupal\Core\Hook\Order\OrderBefore + * @see \Drupal\Core\Hook\Order\OrderAfter + * + * @section ordering_other_module_hooks Ordering other module hook implementations + * + * The order in which hooks implemented in other modules are executed can be + * reordered. The reordering of the targeted hook is done relative to other + * implementations. The reordering process executes after the ordering defined + * in the Hook attribute. + * + * Example of reordering the execution of the 'entity_presave' hook so that + * Content Moderation module hook executes before the Workspaces module hook. + * @code + * #[ReorderHook('entity_presave', + * class: ContentModerationHooks::class, + * method: 'entityPresave', + * order: new OrderBefore(['workspaces']) + * )] + * @endcode + * + * @see \Drupal\Core\Hook\Attribute\ReorderHook + * + * @section removing_hooks Removing hook implementations + * + * The execution of a hooks implemented by other modules can be skipped. This + * is done by removing the targeted hook, use the RemoveHook attribute. + * + * Example of removing the 'help' hook of the Layout Builder module. + * @code + * #[RemoveHook('help', + * class: LayoutBuilderHooks::class, + * method: 'help' + * )] + * @endcode + * + * @see \Drupal\Core\Hook\Attribute\RemoveHook * @} */ diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index 1f9ff1cb0f63489545ab8fd0ebdccfb074426b04..53cf3c95aa5f5cfe1ec0d9899f0cdf1dc19f306d 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -3,10 +3,12 @@ namespace Drupal\Core\Extension; use Drupal\Component\Graph\Graph; +use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Extension\Exception\UnknownExtensionException; use Drupal\Core\Hook\Attribute\LegacyHook; use Drupal\Core\Hook\HookCollectorPass; +use Drupal\Core\Hook\OrderOperation\OrderOperation; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** @@ -58,33 +60,66 @@ class ModuleHandler implements ModuleHandlerInterface { */ protected $includeFileKeys = []; + /** + * Lists of implementation callables by hook. + * + * @var array<string, list<callable>> + */ + protected array $listenersByHook = []; + + /** + * Lists of module names by hook. + * + * The indices are exactly the same as in $listenersByHook. + * + * @var array<string, list<string>> + */ + protected array $modulesByHook = []; + /** * Hook and module keyed list of listeners. * - * @var array + * @var array<string, array<string, list<callable>>> */ protected array $invokeMap = []; + /** + * Ordering rules by hook name. + * + * @var array<string, list<\Drupal\Core\Hook\OrderOperation\OrderOperation>> + */ + protected array $orderingRules = []; + /** * Constructs a ModuleHandler object. * * @param string $root * The app root. - * @param array $module_list + * @param array<string, array{type: string, pathname: string, filename: string}> $module_list * An associative array whose keys are the names of installed modules and * whose values are Extension class parameters. This is normally the * %container.modules% parameter being set up by DrupalKernel. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher * The event dispatcher. - * @param array $hookImplementationsMap + * @param array<string, array<class-string, array<string, string>>> $hookImplementationsMap * 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>> $groupIncludes + * Lists of *.inc file paths that contain procedural implementations, keyed + * by hook name. + * @param array<string, list<string>> $packedOrderOperations + * Ordering rules by hook name, serialized. * * @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 $packedOrderOperations = [], + ) { $this->root = $root; $this->moduleList = []; foreach ($module_list as $name => $module) { @@ -211,6 +246,8 @@ protected function add($type, $name, $path) { foreach ($hook_collector->getImplementations() as $hook => $moduleImplements) { foreach ($moduleImplements as $module => $classImplements) { foreach ($classImplements[ProceduralCall::class] ?? [] as $method) { + $this->listenersByHook[$hook][] = $method; + $this->modulesByHook[$hook][] = $module; $this->invokeMap[$hook][$module][] = $method; } } @@ -310,10 +347,9 @@ public function hasImplementations(string $hook, $modules = NULL): bool { * {@inheritdoc} */ public function invokeAllWith(string $hook, callable $callback): void { - foreach ($this->getHookListeners($hook) as $module => $listeners) { - foreach ($listeners as $listener) { - $callback($listener, $module); - } + foreach ($this->getFlatHookListeners($hook) as $index => $listener) { + $module = $this->modulesByHook[$hook][$index]; + $callback($listener, $module); } } @@ -415,14 +451,6 @@ public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) { // specific variants of it, as in the case of ['form', 'form_FORM_ID']. if (is_array($type)) { $cid = implode(',', $type); - $extra_types = $type; - $type = array_shift($extra_types); - // Allow if statements in this function to use the faster isset() rather - // than !empty() both when $type is passed as a string, or as an array - // with one item. - if (empty($extra_types)) { - unset($extra_types); - } } else { $cid = $type; @@ -432,40 +460,160 @@ public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) { // list of functions to call, and on subsequent calls, iterate through them // quickly. if (!isset($this->alterEventListeners[$cid])) { - $this->alterEventListeners[$cid] = []; - $hook = $type . '_alter'; - $hook_listeners = $this->getHookListeners($hook); - 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; - } - } + $hooks = is_array($type) + ? array_map(static fn (string $type) => $type . '_alter', $type) + : [$type . '_alter']; + $this->alterEventListeners[$cid] = $this->getCombinedListeners($hooks); + } + foreach ($this->alterEventListeners[$cid] as $listener) { + $listener($data, $context1, $context2); + } + } + + /** + * Builds a list of listeners for an alter hook. + * + * @param list<string> $hooks + * The hooks passed to the ->alter() call. + * + * @return list<callable> + * List of implementation callables. + */ + protected function getCombinedListeners(array $hooks): array { + // Get implementation lists for each hook. + $listener_lists = array_map($this->getFlatHookListeners(...), $hooks); + // Remove empty lists. + $listener_lists = array_filter($listener_lists); + if (!$listener_lists) { + // No implementations exist. + return []; + } + if (array_keys($listener_lists) === [0]) { + // Only the first hook has implementations. + return $listener_lists[0]; + } + // Collect the lists from each hook and group the listeners by module. + $listeners_by_identifier = []; + $modules_by_identifier = []; + $identifiers_by_module = []; + foreach ($listener_lists as $i_hook => $listeners) { + $hook = $hooks[$i_hook]; + foreach ($listeners as $i_listener => $listener) { + $module = $this->modulesByHook[$hook][$i_listener]; + $identifier = is_array($listener) + ? get_class($listener[0]) . '::' . $listener[1] + : ProceduralCall::class . '::' . $listener; + $other_module = $modules_by_identifier[$identifier] ?? NULL; + if ($other_module !== NULL) { + $this->triggerErrorForDuplicateAlterHookListener( + $hooks, + $module, + $other_module, + $listener, + $identifier, + ); + // Don't add the same listener more than once. + continue; } + $listeners_by_identifier[$identifier] = $listener; + $modules_by_identifier[$identifier] = $module; + $identifiers_by_module[$module][] = $identifier; } - // 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); - } - foreach ($modules as $module) { - foreach ($hook_listeners[$module] ?? [] as $listener) { - $this->alterEventListeners[$cid][] = $listener; - } + } + // First we get the the modules in moduleList order, this order is module + // weight then alphabetical. Then we apply legacy ordering using + // hook_module_implements_alter(). Finally we order using order attributes. + $modules = array_keys($identifiers_by_module); + $modules = $this->reOrderModulesForAlter($modules, $hooks[0]); + // Create a flat list of identifiers, using the new module order. + $identifiers = array_merge(...array_map( + fn (string $module) => $identifiers_by_module[$module], + $modules, + )); + foreach ($hooks as $hook) { + foreach ($this->getHookOrderingRules($hook) as $rule) { + $rule->apply($identifiers, $modules_by_identifier); + // Order operations must not: + // - Insert duplicate keys. + // - Change the array to be not a list. + // - Add or remove values. + assert($identifiers === array_unique($identifiers)); + assert(array_is_list($identifiers)); + assert(!array_diff($identifiers, array_keys($modules_by_identifier))); + assert(!array_diff(array_keys($modules_by_identifier), $identifiers)); } } - foreach ($this->alterEventListeners[$cid] as $listener) { - $listener($data, $context1, $context2); + return array_map( + static fn (string $identifier) => $listeners_by_identifier[$identifier], + $identifiers, + ); + } + + /** + * Triggers an error on duplicate alter listeners. + * + * This is called when the same method is registered for multiple hooks, which + * are now part of the same alter call. + * + * @param list<string> $hooks + * Hook names from the ->alter() call. + * @param string $module + * The module name for one of the hook implementations. + * @param string $other_module + * The module name for another hook implementation. + * @param callable $listener + * The hook listener. + * @param string $identifier + * String identifier of the hook listener. + */ + protected function triggerErrorForDuplicateAlterHookListener(array $hooks, string $module, string $other_module, callable $listener, string $identifier): void { + $log_message_replacements = [ + '@implementation' => is_array($listener) + ? ('method ' . $identifier . '()') + : ('function ' . $listener[1] . '()'), + '@hooks' => "['" . implode("', '", $hooks) . "']", + ]; + if ($other_module !== $module) { + // There is conflicting information about which module this + // implementation is registered for. At this point we cannot even + // be sure if the module is the one from the main hook or the extra + // hook. This means that ordering may not work as expected and it is + // unclear if the intention is to execute the code multiple times. This + // can be resolved by using a separate method for alter hooks that + // implement on behalf of other modules. + trigger_error((string) new FormattableMarkup( + 'The @implementation is registered for more than one of the alter hooks @hooks from the current ->alter() call, on behalf of different modules @module and @other_module. Only one instance will be part of the implementation list for this hook combination. For the purpose of ordering, the module @module will be used.', + [ + ...$log_message_replacements, + '@module' => "'$module'", + '@other_module' => "'$other_module'", + ], + ), E_USER_WARNING); } + else { + // There is no conflict, but probably one or more redundant #[Hook] + // attributes should be removed. + trigger_error((string) new FormattableMarkup( + 'The @implementation is registered for more than one of the alter hooks @hooks from the current ->alter() call. Only one instance will be part of the implementation list for this hook combination.', + $log_message_replacements, + ), E_USER_NOTICE); + } + } + + /** + * Gets ordering rules for a hook. + * + * @param string $hook + * Hook name. + * + * @return list<\Drupal\Core\Hook\OrderOperation\OrderOperation> + * List of order operations for the hook. + */ + protected function getHookOrderingRules(string $hook): array { + return $this->orderingRules[$hook] ??= array_map( + OrderOperation::unpack(...), + $this->packedOrderOperations[$hook] ?? [], + ); } /** @@ -549,14 +697,39 @@ public function writeCache() { } /** + * Gets hook listeners by module. + * * @param string $hook * The name of the hook. * - * @return array + * @return array<string, list<callable>> * A list of event listeners implementing this hook. */ protected function getHookListeners(string $hook): array { if (!isset($this->invokeMap[$hook])) { + $this->invokeMap[$hook] = []; + foreach ($this->getFlatHookListeners($hook) as $index => $listener) { + $module = $this->modulesByHook[$hook][$index]; + $this->invokeMap[$hook][$module][] = $listener; + } + } + + return $this->invokeMap[$hook] ?? []; + } + + /** + * Gets a list of hook listener callbacks. + * + * @param string $hook + * The hook name. + * + * @return list<callable> + * A list of hook implementation callables. + * + * @internal + */ + protected function getFlatHookListeners(string $hook): array { + if (!isset($this->listenersByHook[$hook])) { foreach ($this->eventDispatcher->getListeners("drupal_hook.$hook") as $listener) { if (is_array($listener) && is_object($listener[0])) { $module = $this->hookImplementationsMap[$hook][get_class($listener[0])][$listener[1]]; @@ -569,7 +742,8 @@ protected function getHookListeners(string $hook): array { $callable = $listener; } if (isset($this->moduleList[$module])) { - $this->invokeMap[$hook][$module][] = $callable; + $this->listenersByHook[$hook][] = $callable; + $this->modulesByHook[$hook][] = $module; } } } @@ -580,7 +754,8 @@ protected function getHookListeners(string $hook): array { } } } - return $this->invokeMap[$hook] ?? []; + + return $this->listenersByHook[$hook] ?? []; } } diff --git a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php index 597da739a14658bfa54167701467e1baf3ebaf64..529fd7275a81f54a45b457b33d4c23a3d8ed5821 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php @@ -225,7 +225,7 @@ public function hasImplementations(string $hook, $modules = NULL): bool; * * @param string $hook * The name of the hook to invoke. - * @param callable $callback + * @param callable(callable, string): mixed $callback * A callable that invokes a hook implementation. Such that * $callback is callable(callable, string): mixed. * Arguments: diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php index 325335615abbb60bfd50ffab6cb817132e92797d..a5f160a8f92ae61afc9fee9c9d9c20ffdb440765 100644 --- a/core/lib/Drupal/Core/Extension/module.api.php +++ b/core/lib/Drupal/Core/Extension/module.api.php @@ -94,6 +94,10 @@ function hook_hook_info(): array { /** * Alter the registry of modules implementing a hook. * + * This hook will be removed in 12.0.0. It is not deprecated in order to + * support the "#[LegacyModuleImplementsAlter]" attribute, used prior to Drupal + * 11.2.0. + * * Only procedural implementations are supported for this hook. * * This hook is invoked in \Drupal::moduleHandler()->getImplementationInfo(). @@ -115,6 +119,8 @@ function hook_hook_info(): array { * file named $module.$group.inc. * @param string $hook * The name of the module hook being implemented. + * + * @see \Drupal\Core\Hook\Attribute\LegacyModuleImplementsAlter */ 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..33da9558b512738519c2aa047a9d96af5a248aac 100644 --- a/core/lib/Drupal/Core/Hook/Attribute/Hook.php +++ b/core/lib/Drupal/Core/Hook/Attribute/Hook.php @@ -4,6 +4,8 @@ namespace Drupal\Core\Hook\Attribute; +use Drupal\Core\Hook\Order\OrderInterface; + /** * Attribute for defining a class method as a hook implementation. * @@ -30,8 +32,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 Drupal\Core\Hook\Order\OrderInterface for more information. + * + * Removing hook implementations can be done by using the attribute + * \Drupal\Core\Hook\Attribute\RemoveHook. + * + * Ordering hook implementations in other modules can be done by using the + * attribute \Drupal\Core\Hook\Attribute\ReorderHook. * * Classes that use this annotation on the class or on their methods are * automatically registered as autowired services with the class name as the @@ -88,7 +96,7 @@ * See \Drupal\Core\Hook\Attribute\LegacyHook for additional information. */ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] -class Hook { +class Hook implements HookAttributeInterface { /** * Constructs a Hook attribute object. @@ -104,23 +112,14 @@ 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\OrderInterface|null $order + * (optional) Set the order of the implementation. */ public function __construct( public string $hook, public string $method = '', public ?string $module = NULL, + public OrderInterface|null $order = NULL, ) {} - /** - * Set the method the hook should apply to. - * - * @param string $method - * The method that the hook attribute applies to. - * This only needs to be set when the attribute is on the class. - */ - public function setMethod(string $method): static { - $this->method = $method; - return $this; - } - } diff --git a/core/lib/Drupal/Core/Hook/Attribute/HookAttributeInterface.php b/core/lib/Drupal/Core/Hook/Attribute/HookAttributeInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..8a2f2413b20fe8553a21381cc36b2588d43ab72f --- /dev/null +++ b/core/lib/Drupal/Core/Hook/Attribute/HookAttributeInterface.php @@ -0,0 +1,15 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook\Attribute; + +/** + * Common interface for attributes used for hook discovery. + * + * This does not imply any shared behavior, it is only used to collect all + * hook-related attributes in the same call. + * + * @internal + */ +interface HookAttributeInterface {} 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..30929be33bc43c575e630b9ba9f37635e78ef9bd --- /dev/null +++ b/core/lib/Drupal/Core/Hook/Attribute/LegacyModuleImplementsAlter.php @@ -0,0 +1,22 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook\Attribute; + +/** + * Prevents procedural hook_module_implements_alter from executing. + * + * This allows the use of the legacy hook_module_implements_alter alongside the + * new attribute-based ordering.Providing support for versions of Drupal older + * than 11.2.0. + * + * 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. + */ +#[\Attribute(\Attribute::TARGET_FUNCTION)] +class LegacyModuleImplementsAlter {} 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..28d4c9456cae920d660cfddb65f0a5b3573706d5 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/Attribute/RemoveHook.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook\Attribute; + +/** + * Removes an already existing implementation. + * + * The effect of this attribute is independent from the specific class or method + * on which it is placed. + */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class RemoveHook implements HookAttributeInterface { + + /** + * Constructs a RemoveHook object. + * + * @param string $hook + * The hook name from which to remove the target implementation. + * @param class-string $class + * The class name of the target hook implementation. + * @param string $method + * The method name of the target hook implementation. + * If the class instance itself is the listener, this should be '__invoke'. + */ + public function __construct( + public readonly string $hook, + public readonly string $class, + public readonly string $method, + ) {} + +} 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..920552ec448e2dffa2b209dd4aee8bf5f9447331 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/Attribute/ReorderHook.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook\Attribute; + +use Drupal\Core\Hook\Order\OrderInterface; + +/** + * Sets the order of an already existing implementation. + * + * The effect of this attribute is independent from the specific class or method + * on which it is placed. + */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class ReorderHook implements HookAttributeInterface { + + /** + * Constructs a ReorderHook object. + * + * @param string $hook + * The hook for which to reorder an implementation. + * @param class-string $class + * The class of the targeted hook implementation. + * @param string $method + * The method name of the targeted hook implementation. + * If the #[Hook] attribute is on the class itself, this should be + * '__invoke'. + * @param \Drupal\Core\Hook\Order\OrderInterface $order + * Specifies a new position for the targeted hook implementation relative to + * other implementations. + */ + public function __construct( + public string $hook, + public string $class, + public string $method, + public OrderInterface $order, + ) {} + +} diff --git a/core/lib/Drupal/Core/Hook/HookCollectorPass.php b/core/lib/Drupal/Core/Hook/HookCollectorPass.php index 3809e24af21d56b20f034b2a45848de6d47e6e93..df2f9f39cfa65249980b627f8c937ccce4b33068 100644 --- a/core/lib/Drupal/Core/Hook/HookCollectorPass.php +++ b/core/lib/Drupal/Core/Hook/HookCollectorPass.php @@ -9,8 +9,13 @@ use Drupal\Component\FileCache\FileCacheFactory; use Drupal\Core\Extension\ProceduralCall; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Attribute\HookAttributeInterface; use Drupal\Core\Hook\Attribute\LegacyHook; +use Drupal\Core\Hook\Attribute\LegacyModuleImplementsAlter; +use Drupal\Core\Hook\Attribute\RemoveHook; +use Drupal\Core\Hook\Attribute\ReorderHook; use Drupal\Core\Hook\Attribute\StopProceduralHookScan; +use Drupal\Core\Hook\OrderOperation\OrderOperation; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -26,29 +31,50 @@ * * Finally, a hook_implementations_map container parameter is added. This * contains a mapping from [hook,class,method] to the module name. + * + * @internal */ class HookCollectorPass implements CompilerPassInterface { /** - * An associative array of hook implementations. + * OOP implementation module names keyed by hook name and "$class::$method". + * + * @var array<string, array<string, string>> + */ + protected array $oopImplementations = []; + + /** + * Procedural implementation module names by hook name. * - * Keys are hook, module, class. Values are a list of methods. + * @var array<string, list<string>> */ - protected array $implementations = []; + protected array $proceduralImplementations = []; /** - * An associative array of hook implementations. + * Order operations grouped by hook name and weight. * - * Keys are hook, module and an empty string value. + * Operations with higher weight are applied last, which means they can + * override the changes from previous operations. * - * @see hook_module_implements_alter() + * @var array<string, array<int, list<\Drupal\Core\Hook\OrderOperation\OrderOperation>>> + * + * @todo Review how to combine operations from different hooks. */ - protected array $moduleImplements = []; + protected array $orderOperations = []; /** - * A list of include files. + * Identifiers to remove, as "$class::$method", keyed by hook name. + * + * @var array<string, list<string>> + */ + protected array $removeHookIdentifiers = []; + + /** + * A map of include files by function name. * * (This is required only for BC.) + * + * @var array<string, string> */ protected array $includes = []; @@ -56,6 +82,8 @@ class HookCollectorPass implements CompilerPassInterface { * A list of functions implementing hook_module_implements_alter(). * * (This is required only for BC.) + * + * @var list<callable-string> */ protected array $moduleImplementsAlters = []; @@ -63,69 +91,248 @@ class HookCollectorPass implements CompilerPassInterface { * A list of functions implementing hook_hook_info(). * * (This is required only for BC.) + * + * @var list<callable-string> */ private array $hookInfo = []; /** - * A list of .inc files. + * Include files, keyed by the $group part of "/$module.$group.inc". + * + * @var array<string, list<string>> */ private array $groupIncludes = []; + /** + * Constructor. + * + * @param list<string> $modules + * Names of installed modules. + * When used as a compiler pass, this parameter should be omitted. + */ + public function __construct( + protected readonly array $modules = [], + ) {} + /** * {@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), + static fn (string $module) => !empty($parameters["$module.hooks_converted"]), + ); + $collector = HookCollectorPass::collectAllHookImplementations($module_list, $skip_procedural_modules); + + $collector->writeToContainer($container); + } + + /** + * Writes collected definitions to the container builder. + * + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + * Container builder. + */ + public function writeToContainer(ContainerBuilder $container): void { $container->register(ProceduralCall::class, ProceduralCall::class) - ->addArgument($collector->includes); + ->addArgument($this->includes); + + // Gather includes for each hook_hook_info group. Store this in + // $groupIncludes so the module handler includes the files at 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(); + + 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) { + + $packed_order_operations = []; + $order_operations = $this->getOrderOperations(); + foreach (preg_grep('@_alter$@', array_keys($order_operations)) as $alter_hook) { + $packed_order_operations[$alter_hook] = array_map( + fn (OrderOperation $operation) => $operation->pack(), + $order_operations[$alter_hook], + ); + } + $definition->setArgument('$packedOrderOperations', $packed_order_operations); + } + + /** + * 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 = []; + foreach ($this->proceduralImplementations as $hook => $procedural_modules) { + foreach ($procedural_modules as $module) { + $implementationsByHook[$hook][ProceduralCall::class . '::' . $module . '_' . $hook] = $module; + } + } + foreach ($this->oopImplementations as $hook => $oopImplementations) { + if (!isset($implementationsByHook[$hook])) { + $implementationsByHook[$hook] = $oopImplementations; + } + else { + $implementationsByHook[$hook] += $oopImplementations; + } + } + foreach ($this->removeHookIdentifiers as $hook => $identifiers_to_remove) { + foreach ($identifiers_to_remove as $identifier_to_remove) { + unset($implementationsByHook[$hook][$identifier_to_remove]); + } + if (empty($implementationsByHook[$hook])) { + unset($implementationsByHook[$hook]); + } + } + return $implementationsByHook; + } + + /** + * Calculates the ordered implementations. + * + * @return array<string, array<string, string>> + * Implementations, as module names keyed by hook name and "$class::$method" + * identifier. + */ + protected function calculateImplementations(): array { + $implementationsByHookOrig = $this->getFilteredImplementations(); + + // List of hooks and modules formatted for hook_module_implements_alter(). + $moduleImplementsMap = []; + foreach ($implementationsByHookOrig as $hook => $hookImplementations) { + foreach (array_intersect($this->modules, $hookImplementations) as $module) { + $moduleImplementsMap[$hook][$module] = ''; + } + } + + $implementationsByHook = []; + foreach ($moduleImplementsMap as $hook => $moduleImplements) { + // 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; } } } + + foreach ($this->getOrderOperations() as $hook => $order_operations) { + self::applyOrderOperations($implementationsByHook[$hook], $order_operations); + } + + return $implementationsByHook; + } + + /** + * Gets order operations by hook. + * + * @return array<string, list<\Drupal\Core\Hook\OrderOperation\OrderOperation>> + * Order operations by hook name. + */ + protected function getOrderOperations(): array { + $operations_by_hook = []; + foreach ($this->orderOperations as $hook => $order_operations_by_weight) { + ksort($order_operations_by_weight); + $operations_by_hook[$hook] = array_merge(...$order_operations_by_weight); + } + return $operations_by_hook; + } + + /** + * Applies order operations to a hook implementation list. + * + * @param array<string, string> $implementation_list + * Implementation list for one hook, as module names keyed by + * "$class::$method" identifiers. + * @param list<\Drupal\Core\Hook\OrderOperation\OrderOperation> $order_operations + * A list of order operations for one hook. + */ + public static function applyOrderOperations(array &$implementation_list, array $order_operations): void { + $module_finder = $implementation_list; + $identifiers = array_keys($module_finder); + foreach ($order_operations as $order_operation) { + $order_operation->apply($identifiers, $module_finder); + assert($identifiers === array_unique($identifiers)); + $identifiers = array_values($identifiers); + } + // Clean up after bad order operations. + $identifiers = array_combine($identifiers, $identifiers); + $identifiers = array_intersect_key($identifiers, $module_finder); + $implementation_list = array_replace($identifiers, $module_finder); + } + + /** + * 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); } /** * Collects all hook implementations. * - * @param array $module_filenames + * @param array<string, array{pathname: string}> $module_list * 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 +341,21 @@ 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 in Drupal + * 12.0.0. */ - public static function collectAllHookImplementations(array $module_filenames, ?ContainerBuilder $container = NULL): static { - $modules = array_map(fn ($x) => preg_quote($x, '/'), array_keys($module_filenames)); - // Longer modules first. - usort($modules, fn($a, $b) => strlen($b) - strlen($a)); - $module_preg = '/^(?<function>(?<module>' . implode('|', $modules) . ')_(?!preprocess_)(?!update_\d)(?<hook>[a-zA-Z0-9_\x80-\xff]+$))/'; - $collector = new static(); - foreach ($module_filenames as $module => $info) { - $skip_procedural = FALSE; - if ($container?->hasParameter("$module.hooks_converted")) { - $skip_procedural = $container->getParameter("$module.hooks_converted"); - } + public static function collectAllHookImplementations(array $module_list, array $skipProceduralModules = []): static { + $modules = array_keys($module_list); + $modules_by_length = $modules; + usort($modules_by_length, static fn ($a, $b) => strlen($b) - strlen($a)); + $known_modules_pattern = implode('|', array_map( + static fn ($x) => preg_quote($x, '/'), + $modules_by_length, + )); + $module_preg = '/^(?<function>(?<module>' . $known_modules_pattern . ')_(?!preprocess_)(?!update_\d)(?<hook>[a-zA-Z0-9_\x80-\xff]+$))/'; + $collector = new static($modules); + foreach ($module_list as $module => $info) { + $skip_procedural = in_array($module, $skipProceduralModules); $collector->collectModuleHookImplementations(dirname($info['pathname']), $module, $module_preg, $skip_procedural); } return $collector; @@ -195,16 +403,33 @@ 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->oopImplementations[$attribute->hook][$class . '::' . ($attribute->method ?: $method)] = $attribute->module ?? $module; + if ($attribute->order !== NULL) { + // Use a lower weight for order operations that are declared + // together with the hook listener they apply to. + $this->orderOperations[$attribute->hook][0][] = $attribute->order->getOperation("$class::$method"); + } + } + elseif ($attribute instanceof ReorderHook) { + // Use a higher weight for order operations that target other hook + // listeners. + $this->orderOperations[$attribute->hook][1][] = $attribute->order->getOperation($attribute->class . '::' . $attribute->method); + } + elseif ($attribute instanceof RemoveHook) { + $this->removeHookIdentifiers[$attribute->hook][] = $attribute->class . '::' . $attribute->method; + } + } } } elseif (!$skip_procedural) { @@ -217,14 +442,15 @@ 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)) { - $implementations[] = ['function' => $function, 'module' => $matches['module'], 'hook' => $matches['hook']]; + if (!StaticReflectionParser::hasAttribute($attributes, LegacyHook::class) && preg_match($module_preg, $function, $matches) && !StaticReflectionParser::hasAttribute($attributes, LegacyModuleImplementsAlter::class)) { + assert($function === $matches['module'] . '_' . $matches['hook']); + $implementations[] = ['module' => $matches['module'], 'hook' => $matches['hook']]; } } $procedural_hook_file_cache->set($filename, $implementations); } foreach ($implementations as $implementation) { - $this->addProceduralImplementation($fileinfo, $implementation['hook'], $implementation['module'], $implementation['function']); + $this->addProceduralImplementation($fileinfo, $implementation['hook'], $implementation['module']); } } if ($extension === 'inc') { @@ -250,89 +476,33 @@ protected static function filterIterator(\SplFileInfo $fileInfo, $key, \Recursiv return TRUE; } // glob() doesn't support streams but scandir() does. - return !in_array($fileInfo->getFilename(), ['tests', 'js', 'css']) && !array_filter(scandir($key), fn ($filename) => str_ends_with($filename, '.info.yml')); + return !in_array($fileInfo->getFilename(), ['tests', 'js', 'css']) && !array_filter(scandir($key), static fn ($filename) => str_ends_with($filename, '.info.yml')); } 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!) - * @param string $function - * The name of function implementing the hook. (Wow!) + * The module implementing the hook, or on behalf of which the hook is + * implemented. */ - protected function addProceduralImplementation(\SplFileInfo $fileinfo, string $hook, string $module, string $function): void { - $this->addFromAttribute(new Hook($hook, $module . '_' . $hook), ProceduralCall::class, $module); + protected function addProceduralImplementation(\SplFileInfo $fileinfo, string $hook, string $module): void { + $function = $module . '_' . $hook; 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->proceduralImplementations[$hook][] = $module; if ($fileinfo->getExtension() !== 'module') { $this->includes[$function] = $fileinfo->getPathname(); } @@ -341,6 +511,8 @@ protected function addProceduralImplementation(\SplFileInfo $fileinfo, string $h /** * This method is only to be used by ModuleHandler. * + * @todo Remove when ModuleHandler::add() is removed in Drupal 12.0.0. + * * @internal */ public function loadAllIncludes(): void { @@ -352,21 +524,40 @@ public function loadAllIncludes(): void { /** * This method is only to be used by ModuleHandler. * + * @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 in Drupal 12.0.0. + * * @internal */ public function getImplementations(): array { - return $this->implementations; + $implementationsByHook = $this->getFilteredImplementations(); + + // List of modules implementing hooks with the implementation details. + $implementations = []; + + foreach ($implementationsByHook as $hook => $hookImplementations) { + foreach ($this->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 +570,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\Attribute\HookAttributeInterface>> + * Lists of Hook attribute instances by method name. + */ + protected static function getAttributeInstances(\ReflectionClass $reflectionClass): array { + $attributes = []; + $reflections = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); + $reflections[] = $reflectionClass; + foreach ($reflections as $reflection) { + if ($reflectionAttributes = $reflection->getAttributes(HookAttributeInterface::class, \ReflectionAttribute::IS_INSTANCEOF)) { + $method = $reflection instanceof \ReflectionMethod ? $reflection->getName() : '__invoke'; + $attributes[$method] = array_map(static fn (\ReflectionAttribute $ra) => $ra->newInstance(), $reflectionAttributes); + } } + return $attributes; } } diff --git a/core/lib/Drupal/Core/Hook/Order/Order.php b/core/lib/Drupal/Core/Hook/Order/Order.php new file mode 100644 index 0000000000000000000000000000000000000000..6a7934df7d2a02fae9c830856a2af5270b6505d1 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/Order/Order.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook\Order; + +use Drupal\Core\Hook\OrderOperation\FirstOrLast; +use Drupal\Core\Hook\OrderOperation\OrderOperation; + +/** + * Set this implementation to be first or last. + */ +enum Order: int implements OrderInterface { + + // This implementation should execute first. + case First = 1; + + // This implementation should execute last. + case Last = 0; + + /** + * {@inheritdoc} + */ + public function getOperation(string $identifier): OrderOperation { + return new FirstOrLast($identifier, $this === self::Last); + } + +} diff --git a/core/lib/Drupal/Core/Hook/Order/OrderAfter.php b/core/lib/Drupal/Core/Hook/Order/OrderAfter.php new file mode 100644 index 0000000000000000000000000000000000000000..73dfd926475569138f81976c5c57e4dec7f53d13 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/Order/OrderAfter.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook\Order; + +/** + * Set this implementation to be after others. + */ +readonly class OrderAfter extends RelativeOrderBase { + + /** + * {@inheritdoc} + */ + protected function isAfter(): bool { + return TRUE; + } + +} diff --git a/core/lib/Drupal/Core/Hook/Order/OrderBefore.php b/core/lib/Drupal/Core/Hook/Order/OrderBefore.php new file mode 100644 index 0000000000000000000000000000000000000000..cc79560a3d5ab70d3b0087ef50c70b0b09b452e3 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/Order/OrderBefore.php @@ -0,0 +1,19 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook\Order; + +/** + * Set this implementation to be before others. + */ +readonly class OrderBefore extends RelativeOrderBase { + + /** + * {@inheritdoc} + */ + protected function isAfter(): bool { + return FALSE; + } + +} diff --git a/core/lib/Drupal/Core/Hook/Order/OrderInterface.php b/core/lib/Drupal/Core/Hook/Order/OrderInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..85fc820a5c61466255678ebe933666a364ab15b0 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/Order/OrderInterface.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Core\Hook\Order; + +use Drupal\Core\Hook\OrderOperation\OrderOperation; + +/** + * Interface for order specifiers used in hook attributes. + * + * Objects implementing this interface allow for relative ordering of hooks. + * These objects are passed as an order parameter to a Hook or ReorderHook + * attribute. + * Order::First and Order::Last are simple order operations that move the hook + * implementation to the first or last position of hooks at the time the order + * directive is executed. + * @code + * #[Hook('custom_hook', order: Order::First)] + * @endcode + * OrderBefore and OrderAfter take additional parameters + * for ordering. See Drupal\Core\Hook\Order\RelativeOrderBase. + * @code + * #[Hook('custom_hook', order: new OrderBefore(['other_module']))] + * @endcode + */ +interface OrderInterface { + + /** + * Gets order operations specified by this object. + * + * @param string $identifier + * Identifier of the implementation to move to a new position. The format + * is the class followed by "::" then the method name. For example, + * "Drupal\my_module\Hook\MyModuleHooks::methodName". + * + * @return \Drupal\Core\Hook\OrderOperation\OrderOperation + * Order operation to apply to a hook implementation list. + */ + public function getOperation(string $identifier): OrderOperation; + +} diff --git a/core/lib/Drupal/Core/Hook/Order/RelativeOrderBase.php b/core/lib/Drupal/Core/Hook/Order/RelativeOrderBase.php new file mode 100644 index 0000000000000000000000000000000000000000..eaf6eade668d7d94d3c7e2560233d5720b82aa63 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/Order/RelativeOrderBase.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Core\Hook\Order; + +use Drupal\Core\Hook\OrderOperation\BeforeOrAfter; +use Drupal\Core\Hook\OrderOperation\OrderOperation; + +/** + * Orders an implementation relative to other implementations. + */ +abstract readonly class RelativeOrderBase implements OrderInterface { + + /** + * Constructor. + * + * @param list<string> $modules + * A list of modules the implementations should order against. + * @param list<array{class-string, string}> $classesAndMethods + * A list of implementations to order against, as [$class, $method]. + */ + public function __construct( + public array $modules = [], + public array $classesAndMethods = [], + ) { + if (!$this->modules && !$this->classesAndMethods) { + throw new \LogicException('Order must provide either modules or class-method pairs to order against.'); + } + } + + /** + * Specifies the ordering direction. + * + * @return bool + * TRUE, if the ordered implementation should be inserted after the + * implementations specified in the constructor. + */ + abstract protected function isAfter(): bool; + + /** + * {@inheritdoc} + */ + public function getOperation(string $identifier): OrderOperation { + return new BeforeOrAfter( + $identifier, + $this->modules, + array_map( + static fn(array $class_and_method) => implode('::', $class_and_method), + $this->classesAndMethods, + ), + $this->isAfter(), + ); + } + +} diff --git a/core/lib/Drupal/Core/Hook/OrderOperation/BeforeOrAfter.php b/core/lib/Drupal/Core/Hook/OrderOperation/BeforeOrAfter.php new file mode 100644 index 0000000000000000000000000000000000000000..e87f661fc39257fc19b841404aa6981bf6c5e24d --- /dev/null +++ b/core/lib/Drupal/Core/Hook/OrderOperation/BeforeOrAfter.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Core\Hook\OrderOperation; + +/** + * Moves one listener to be called before or after other listeners. + * + * @internal + */ +class BeforeOrAfter extends OrderOperation { + + /** + * Constructor. + * + * @param string $identifier + * Identifier of the implementation to move to a new position. The format + * is the class followed by "::" then the method name. For example, + * "Drupal\my_module\Hook\MyModuleHooks::methodName". + * @param list<string> $modulesToOrderAgainst + * Module names of listeners to order against. + * @param list<string> $identifiersToOrderAgainst + * Identifiers of listeners to order against. + * The format is "$class::$method". + * @param bool $isAfter + * TRUE, if the listener to move should be moved after the listener to order + * against, FALSE if it should be moved before. + */ + public function __construct( + protected readonly string $identifier, + protected readonly array $modulesToOrderAgainst, + protected readonly array $identifiersToOrderAgainst, + protected readonly bool $isAfter, + ) {} + + /** + * {@inheritdoc} + */ + public function apply(array &$identifiers, array $module_finder): void { + assert(array_is_list($identifiers)); + $index = array_search($this->identifier, $identifiers); + if ($index === FALSE) { + // Nothing to reorder. + return; + } + $identifiers_to_order_against = $this->identifiersToOrderAgainst; + if ($this->modulesToOrderAgainst) { + $identifiers_to_order_against = [ + ...$identifiers_to_order_against, + ...array_keys(array_intersect($module_finder, $this->modulesToOrderAgainst)), + ]; + } + $indices_to_order_against = array_keys(array_intersect($identifiers, $identifiers_to_order_against)); + if ($indices_to_order_against === []) { + return; + } + if ($this->isAfter) { + $max_index_to_order_against = max($indices_to_order_against); + if ($index >= $max_index_to_order_against) { + // The element is already after the other elements. + return; + } + array_splice($identifiers, $max_index_to_order_against + 1, 0, $this->identifier); + // Remove the element after splicing. + unset($identifiers[$index]); + $identifiers = array_values($identifiers); + } + else { + $min_index_to_order_against = min($indices_to_order_against); + if ($index <= $min_index_to_order_against) { + // The element is already before the other elements. + return; + } + // Remove the element before splicing. + unset($identifiers[$index]); + array_splice($identifiers, $min_index_to_order_against, 0, $this->identifier); + } + } + +} diff --git a/core/lib/Drupal/Core/Hook/OrderOperation/FirstOrLast.php b/core/lib/Drupal/Core/Hook/OrderOperation/FirstOrLast.php new file mode 100644 index 0000000000000000000000000000000000000000..2169e533891da8255b721069020207d5dd7965e1 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/OrderOperation/FirstOrLast.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Core\Hook\OrderOperation; + +/** + * Moves one listener to the start or end of the list. + * + * @internal + */ +class FirstOrLast extends OrderOperation { + + /** + * Constructor. + * + * @param string $identifier + * Identifier of the implementation to move to a new position. The format + * is the class followed by "::" then the method name. For example, + * "Drupal\my_module\Hook\MyModuleHooks::methodName". + * @param bool $isLast + * TRUE to move to the end, FALSE to move to the start. + */ + public function __construct( + protected readonly string $identifier, + protected readonly bool $isLast, + ) {} + + /** + * {@inheritdoc} + */ + public function apply(array &$identifiers, array $module_finder): void { + $index = array_search($this->identifier, $identifiers); + if ($index === FALSE) { + // The element does not exist. + return; + } + unset($identifiers[$index]); + if ($this->isLast) { + $identifiers[] = $this->identifier; + } + else { + $identifiers = [$this->identifier, ...$identifiers]; + } + $identifiers = array_values($identifiers); + } + +} diff --git a/core/lib/Drupal/Core/Hook/OrderOperation/OrderOperation.php b/core/lib/Drupal/Core/Hook/OrderOperation/OrderOperation.php new file mode 100644 index 0000000000000000000000000000000000000000..617b05c7e72b3fac682db315f8a31d80172889a7 --- /dev/null +++ b/core/lib/Drupal/Core/Hook/OrderOperation/OrderOperation.php @@ -0,0 +1,41 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Core\Hook\OrderOperation; + +/** + * Base class for order operations. + */ +abstract class OrderOperation { + + /** + * Converts the operation to a structure that can be stored in the container. + * + * @return array + * Packed operation. + */ + final public function pack(): array { + $is_before_or_after = match(get_class($this)) { + BeforeOrAfter::class => TRUE, + FirstOrLast::class => FALSE, + }; + return [$is_before_or_after, get_object_vars($this)]; + } + + /** + * Converts the stored operation to objects that can apply ordering rules. + * + * @param array $packed_operation + * Packed operation. + * + * @return self + * Unpacked operation. + */ + final public static function unpack(array $packed_operation): self { + [$is_before_or_after, $args] = $packed_operation; + $class = $is_before_or_after ? BeforeOrAfter::class : FirstOrLast::class; + return new $class(...$args); + } + +} diff --git a/core/modules/ckeditor5/ckeditor5.module b/core/modules/ckeditor5/ckeditor5.module index 759900cda00c11169d17aefbd70b8b758a4350ac..86be6f62b2471e314cd517a28aced049ff35605a 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..7eb7601e3d8bc08dbbfd1f696f9cf5fb14c56ba6 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\Order\OrderAfter; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Asset\AttachedAssetsInterface; use Drupal\Core\Render\Element; @@ -100,8 +101,19 @@ 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'], + ) + )] 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 97bdc664e2591c76bf2c40039358722e4eb7f653..49b15bb022bcd68d7b5a6bde8e0794f9c0e879ac 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..c681b07c0596271a3600491f724502802cfa87b2 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\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..28159482d3163382def10ec1a64a7909313dbdd7 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\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..397eedc8dad78d4c0ccd8076ac0e6fde4094732a 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\Order\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..3b94bc845733df3c27ac65a567916070a9d1dc23 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\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/HookCollector/aaa_hook_collector_test/aaa_hook_collector_test.info.yml b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/aaa_hook_collector_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..0ebae91b673501b386fdca9c31515021c192c01a --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/aaa_hook_collector_test.info.yml @@ -0,0 +1,7 @@ +name: AAA Hook collector test +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/HookCollector/aaa_hook_collector_test/src/Hook/TestHookAfter.php b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookAfter.php new file mode 100644 index 0000000000000000000000000000000000000000..bf2ae7db46d3e7caed95d427c6d3d6449015f133 --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookAfter.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\aaa_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order\OrderAfter; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class TestHookAfter { + + /** + * This pair tests OrderAfter. + */ + #[Hook('custom_hook_test_hook_after', order: new OrderAfter(['bbb_hook_collector_test']))] + public function hookAfter(): string { + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookAfterClassMethod.php b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookAfterClassMethod.php new file mode 100644 index 0000000000000000000000000000000000000000..e3861fe3516bd68433af2b84c7195204bef47f36 --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookAfterClassMethod.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\aaa_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order\OrderAfter; +use Drupal\bbb_hook_collector_test\Hook\TestHookAfterClassMethod as TestHookAfterClassMethodForAfter; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +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(): string { + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookBefore.php b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookBefore.php new file mode 100644 index 0000000000000000000000000000000000000000..76661c5297d4adee4cf1fa7ecee9c3a9fdfc0057 --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookBefore.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\aaa_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class TestHookBefore { + + /** + * This pair tests OrderBefore. + */ + #[Hook('custom_hook_test_hook_before')] + public function hookBefore(): string { + // This should be run second, there is another hook reordering before this. + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookFirst.php b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookFirst.php new file mode 100644 index 0000000000000000000000000000000000000000..e3ce6b86963a1209fa37b3ec2fd979db74243107 --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookFirst.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\aaa_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class TestHookFirst { + + /** + * This pair tests OrderFirst. + */ + #[Hook('custom_hook_test_hook_first')] + public function hookFirst(): string { + // This should be run second, there is another hook reordering before this. + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookLast.php b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookLast.php new file mode 100644 index 0000000000000000000000000000000000000000..427422b378f538b89ee19f27fe04fcf5b0fd1802 --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookLast.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\aaa_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order\Order; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class TestHookLast { + + /** + * This pair tests OrderLast. + */ + #[Hook('custom_hook_test_hook_last', order: Order::Last)] + public function hookLast(): string { + // This should be run after. + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookOrderExtraTypes.php b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookOrderExtraTypes.php new file mode 100644 index 0000000000000000000000000000000000000000..eb2c35472f0382581b6dffedfd5fac0363ecd39c --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookOrderExtraTypes.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\aaa_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order\OrderAfter; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class TestHookOrderExtraTypes { + + /** + * This pair tests OrderAfter with ExtraTypes. + */ + #[Hook('custom_hook_extra_types1_alter', + order: new OrderAfter( + modules: ['bbb_hook_collector_test'], + ) + )] + public function customHookExtraTypes(array &$calls): void { + // This should be run after. + $calls[] = __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookReorderHookFirst.php b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookReorderHookFirst.php new file mode 100644 index 0000000000000000000000000000000000000000..55e12aad240f97737975c8e55f28f39cadcc93d7 --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/aaa_hook_collector_test/src/Hook/TestHookReorderHookFirst.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\aaa_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order\OrderAfter; +use Drupal\Core\Hook\Attribute\ReorderHook; +use Drupal\bbb_hook_collector_test\Hook\TestHookReorderHookLast; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +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 function customHookOverride(): string { + // This normally would run first. + // We override that order in hook_order_second_alphabetically. + // We override, that order here with ReorderHook. + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/bbb_hook_collector_test.info.yml b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/bbb_hook_collector_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..a084845fb14fc3230650cb17288cf7b3a74b63e5 --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/bbb_hook_collector_test.info.yml @@ -0,0 +1,7 @@ +name: BBB Hook collector test +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/HookCollector/bbb_hook_collector_test/src/Hook/TestHookAfter.php b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookAfter.php new file mode 100644 index 0000000000000000000000000000000000000000..89a6f3a1b05b2212fde3932c4a304995bd69eff5 --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookAfter.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\bbb_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class TestHookAfter { + + /** + * This pair tests OrderAfter. + */ + #[Hook('custom_hook_test_hook_after')] + public function hookAfter(): string { + // This should be run before. + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookAfterClassMethod.php b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookAfterClassMethod.php new file mode 100644 index 0000000000000000000000000000000000000000..1cedcdcaa66c0f7336779a40b45ba8a17a7cae96 --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookAfterClassMethod.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\bbb_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class TestHookAfterClassMethod { + + /** + * This pair tests OrderAfter with a passed class and method. + */ + #[Hook('custom_hook_test_hook_after_class_method')] + public static function hookAfterClassMethod(): string { + // This should be run first since another hook overrides the natural order. + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookBefore.php b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookBefore.php new file mode 100644 index 0000000000000000000000000000000000000000..12341cd27d21b0f241646aa74b1a2c42dcdf0da8 --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookBefore.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\bbb_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order\OrderBefore; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class TestHookBefore { + + /** + * This pair tests OrderBefore. + */ + #[Hook('custom_hook_test_hook_before', order: new OrderBefore(['aaa_hook_collector_test']))] + public function hookBefore(): string { + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookFirst.php b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookFirst.php new file mode 100644 index 0000000000000000000000000000000000000000..ac72de1da8042fe4c269e689ec3eeb190752be9a --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookFirst.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\bbb_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order\Order; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class TestHookFirst { + + /** + * This pair tests OrderFirst. + */ + #[Hook('custom_hook_test_hook_first', order: Order::First)] + public function hookFirst(): string { + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookLast.php b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookLast.php new file mode 100644 index 0000000000000000000000000000000000000000..6b30344f583d888557064529530c06e96ff7ec55 --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookLast.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\bbb_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class TestHookLast { + + /** + * This pair tests OrderLast. + */ + #[Hook('custom_hook_test_hook_last')] + public function hookLast(): string { + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookOrderExtraTypes.php b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookOrderExtraTypes.php new file mode 100644 index 0000000000000000000000000000000000000000..a32e3529ec6f0c5c6164e3f953bb1bca2ebfbebf --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookOrderExtraTypes.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\bbb_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class TestHookOrderExtraTypes { + + /** + * This pair tests OrderAfter with ExtraTypes. + */ + #[Hook('custom_hook_extra_types2_alter')] + public function customHookExtraTypes(array &$calls): void { + $calls[] = __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookReorderHookLast.php b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookReorderHookLast.php new file mode 100644 index 0000000000000000000000000000000000000000..3b89394347a8981e87586906e243748ab06a3399 --- /dev/null +++ b/core/modules/system/tests/modules/HookCollector/bbb_hook_collector_test/src/Hook/TestHookReorderHookLast.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\bbb_hook_collector_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order\Order; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class TestHookReorderHookLast { + + /** + * This pair tests ReorderHook. + */ + #[Hook('custom_hook_override', order: Order::First)] + public function customHookOverride(): string { + // This normally would run second. + // We override that order here with Order::First. + // We override, that order in aaa_hook_collector_test with + // ReorderHook. + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/aaa_hook_order_test.info.yml b/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/aaa_hook_order_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..0cc98c51c0b6f29b5374336da36586aef4312a43 --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/aaa_hook_order_test.info.yml @@ -0,0 +1,6 @@ +name: AAA Hook order test +type: module +description: 'Test module used to test hook ordering.' +package: Testing +version: VERSION +core_version_requirement: '*' diff --git a/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/aaa_hook_order_test.module b/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/aaa_hook_order_test.module new file mode 100644 index 0000000000000000000000000000000000000000..513a72c21f4413c00bad3635b63876935d9d2046 --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/aaa_hook_order_test.module @@ -0,0 +1,45 @@ +<?php + +/** + * @file + * Contains procedural hook implementations. + */ + +declare(strict_types=1); + +use Drupal\aaa_hook_order_test\Hook\ModuleImplementsAlter; + +/** + * Implements hook_test_hook(). + */ +function aaa_hook_order_test_test_hook(): string { + return __FUNCTION__; +} + +/** + * Implements hook_sparse_test_hook(). + */ +function aaa_hook_order_test_sparse_test_hook(): string { + return __FUNCTION__; +} + +/** + * Implements hook_procedural_alter(). + */ +function aaa_hook_order_test_procedural_alter(array &$calls): void { + $calls[] = __FUNCTION__; +} + +/** + * Implements hook_procedural_subtype_alter(). + */ +function aaa_hook_order_test_procedural_subtype_alter(array &$calls): void { + $calls[] = __FUNCTION__; +} + +/** + * Implements hook_module_implements_alter(). + */ +function aaa_hook_order_test_module_implements_alter(array &$implementations, string $hook): void { + ModuleImplementsAlter::call($implementations, $hook); +} diff --git a/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/src/Hook/AAlterHooks.php b/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/src/Hook/AAlterHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..f9e124e217b2c61120422a8b6f8b510a145e1691 --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/src/Hook/AAlterHooks.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\aaa_hook_order_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order\OrderAfter; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class AAlterHooks { + + #[Hook('test_alter', order: new OrderAfter(modules: ['ccc_hook_order_test']))] + public function testAlterAfterC(array &$calls): void { + $calls[] = __METHOD__; + } + + #[Hook('test_subtype_alter')] + public function testSubtypeAlter(array &$calls): void { + $calls[] = __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/src/Hook/AHooks.php b/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/src/Hook/AHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..234264cbbe55a43d928ec02ad18c9fed81668897 --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/src/Hook/AHooks.php @@ -0,0 +1,40 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\aaa_hook_order_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order\Order; +use Drupal\Core\Hook\Order\OrderAfter; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class AHooks { + + #[Hook('test_hook')] + public function testHook(): string { + return __METHOD__; + } + + #[Hook('test_hook', order: Order::First)] + public function testHookFirst(): string { + return __METHOD__; + } + + #[Hook('test_hook', order: Order::Last)] + public function testHookLast(): string { + return __METHOD__; + } + + #[Hook('test_hook', order: new OrderAfter(modules: ['bbb_hook_order_test']))] + public function testHookAfterB(): string { + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/src/Hook/ModuleImplementsAlter.php b/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/src/Hook/ModuleImplementsAlter.php new file mode 100644 index 0000000000000000000000000000000000000000..76d3912bdebef389be940431855958a9c9633a45 --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/aaa_hook_order_test/src/Hook/ModuleImplementsAlter.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\aaa_hook_order_test\Hook; + +/** + * Contains a replaceable callback for hook_module_implements_alter(). + */ +class ModuleImplementsAlter { + + /** + * Callback for hook_module_implements_alter(). + * + * @var ?\Closure + * @phpstan-var (\Closure(array<string, string|false>&, string): void)|null + */ + private static ?\Closure $callback = NULL; + + /** + * Sets a callback for hook_module_implements_alter(). + * + * @param ?\Closure $callback + * Callback to set, or NULL to unset. + * + * @phpstan-param (\Closure(array<string, string|false>&, string): void)|null $callback + */ + public static function set(?\Closure $callback): void { + self::$callback = $callback; + } + + /** + * Invokes the registered callback. + * + * @param array<string, string|false> $implementations + * The implementations, as "group" by module name. + * @param string $hook + * The hook. + */ + public static function call(array &$implementations, string $hook): void { + if (self::$callback === NULL) { + return; + } + (self::$callback)($implementations, $hook); + } + +} diff --git a/core/modules/system/tests/modules/HookOrder/bbb_hook_order_test/bbb_hook_order_test.info.yml b/core/modules/system/tests/modules/HookOrder/bbb_hook_order_test/bbb_hook_order_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..4b0667172e64732ccba5c7858048291b8bffc98a --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/bbb_hook_order_test/bbb_hook_order_test.info.yml @@ -0,0 +1,6 @@ +name: BBB Hook order test +type: module +description: 'Test module used to test hook ordering.' +package: Testing +version: VERSION +core_version_requirement: '*' diff --git a/core/modules/system/tests/modules/HookOrder/bbb_hook_order_test/bbb_hook_order_test.module b/core/modules/system/tests/modules/HookOrder/bbb_hook_order_test/bbb_hook_order_test.module new file mode 100644 index 0000000000000000000000000000000000000000..c2ac51b2f29c9f2a89d933cf0afa55202a17faad --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/bbb_hook_order_test/bbb_hook_order_test.module @@ -0,0 +1,29 @@ +<?php + +/** + * @file + * Contains procedural hook implementations. + */ + +declare(strict_types=1); + +/** + * Implements hook_test_hook(). + */ +function bbb_hook_order_test_test_hook(): string { + return __FUNCTION__; +} + +/** + * Implements hook_procedural_alter(). + */ +function bbb_hook_order_test_procedural_alter(array &$calls): void { + $calls[] = __FUNCTION__; +} + +/** + * Implements hook_procedural_subtype_alter(). + */ +function bbb_hook_order_test_procedural_subtype_alter(array &$calls): void { + $calls[] = __FUNCTION__; +} diff --git a/core/modules/system/tests/modules/HookOrder/bbb_hook_order_test/src/Hook/BAlterHooks.php b/core/modules/system/tests/modules/HookOrder/bbb_hook_order_test/src/Hook/BAlterHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..0be826094ebfc3e664ec9691eed63a9fd424786a --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/bbb_hook_order_test/src/Hook/BAlterHooks.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\bbb_hook_order_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class BAlterHooks { + + #[Hook('test_subtype_alter')] + public function testSubtypeAlter(array &$calls): void { + $calls[] = __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookOrder/bbb_hook_order_test/src/Hook/BHooks.php b/core/modules/system/tests/modules/HookOrder/bbb_hook_order_test/src/Hook/BHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..a93643e45e4b7f9398479705bbfe8e408a04e3e6 --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/bbb_hook_order_test/src/Hook/BHooks.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\bbb_hook_order_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class BHooks { + + #[Hook('test_hook')] + public function testHook(): string { + return __METHOD__; + } + + #[Hook('sparse_test_hook')] + public function sparseTestHook(): string { + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookOrder/ccc_hook_order_test/ccc_hook_order_test.info.yml b/core/modules/system/tests/modules/HookOrder/ccc_hook_order_test/ccc_hook_order_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..d20a7a36ab1fe70abfb53ef2e952471529b919f0 --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/ccc_hook_order_test/ccc_hook_order_test.info.yml @@ -0,0 +1,6 @@ +name: CCC Hook order test +type: module +description: 'Test module used to test hook ordering.' +package: Testing +version: VERSION +core_version_requirement: '*' diff --git a/core/modules/system/tests/modules/HookOrder/ccc_hook_order_test/ccc_hook_order_test.module b/core/modules/system/tests/modules/HookOrder/ccc_hook_order_test/ccc_hook_order_test.module new file mode 100644 index 0000000000000000000000000000000000000000..3c6246298b727bb88d9be32fda2e1f92e9693a63 --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/ccc_hook_order_test/ccc_hook_order_test.module @@ -0,0 +1,36 @@ +<?php + +/** + * @file + * Contains procedural hook implementations. + */ + +declare(strict_types=1); + +/** + * Implements hook_test_hook(). + */ +function ccc_hook_order_test_test_hook(): string { + return __FUNCTION__; +} + +/** + * Implements hook_sparse_test_hook(). + */ +function ccc_hook_order_test_sparse_test_hook(): string { + return __FUNCTION__; +} + +/** + * Implements hook_procedural_alter(). + */ +function ccc_hook_order_test_procedural_alter(array &$calls): void { + $calls[] = __FUNCTION__; +} + +/** + * Implements hook_procedural_subtype_alter(). + */ +function ccc_hook_order_test_procedural_subtype_alter(array &$calls): void { + $calls[] = __FUNCTION__; +} diff --git a/core/modules/system/tests/modules/HookOrder/ccc_hook_order_test/src/Hook/CAlterHooks.php b/core/modules/system/tests/modules/HookOrder/ccc_hook_order_test/src/Hook/CAlterHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..f005daa0a38b5547d8007251a895a1a6b6ade7c4 --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/ccc_hook_order_test/src/Hook/CAlterHooks.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\ccc_hook_order_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class CAlterHooks { + + #[Hook('test_alter')] + public function testAlter(array &$calls): void { + $calls[] = __METHOD__; + } + + #[Hook('test_subtype_alter')] + public function testSubtypeAlter(array &$calls): void { + $calls[] = __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookOrder/ccc_hook_order_test/src/Hook/CHooks.php b/core/modules/system/tests/modules/HookOrder/ccc_hook_order_test/src/Hook/CHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..b3571d490832834a0cf58b78e5e0a5e59861709f --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/ccc_hook_order_test/src/Hook/CHooks.php @@ -0,0 +1,49 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\ccc_hook_order_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Order\Order; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. Some of the implementations are reordered + * using order attributes. + */ +class CHooks { + + #[Hook('test_hook')] + public function testHook(): string { + return __METHOD__; + } + + #[Hook('test_hook', order: Order::First)] + public function testHookFirst(): string { + return __METHOD__; + } + + /** + * This implementation is reordered from elsewhere. + * + * @see \Drupal\ddd_hook_order_test\Hook\DHooks + */ + #[Hook('test_hook')] + public function testHookReorderFirst(): string { + return __METHOD__; + } + + /** + * This implementation is removed from elsewhere. + * + * @see \Drupal\ddd_hook_order_test\Hook\DHooks + */ + #[Hook('test_hook')] + public function testHookRemoved(): string { + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookOrder/ddd_hook_order_test/ddd_hook_order_test.info.yml b/core/modules/system/tests/modules/HookOrder/ddd_hook_order_test/ddd_hook_order_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..df2c987a667260c26fbd0de3355e817ed120ade0 --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/ddd_hook_order_test/ddd_hook_order_test.info.yml @@ -0,0 +1,6 @@ +name: DDD Hook order test +type: module +description: 'Test module used to test hook ordering.' +package: Testing +version: VERSION +core_version_requirement: '*' diff --git a/core/modules/system/tests/modules/HookOrder/ddd_hook_order_test/ddd_hook_order_test.module b/core/modules/system/tests/modules/HookOrder/ddd_hook_order_test/ddd_hook_order_test.module new file mode 100644 index 0000000000000000000000000000000000000000..82cccc7ba6c3ada03a485d01fcd0a5d3aa37ce1c --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/ddd_hook_order_test/ddd_hook_order_test.module @@ -0,0 +1,15 @@ +<?php + +/** + * @file + * Contains procedural hook implementations. + */ + +declare(strict_types=1); + +/** + * Implements hook_test_hook(). + */ +function ddd_hook_order_test_test_hook(): string { + return __FUNCTION__; +} diff --git a/core/modules/system/tests/modules/HookOrder/ddd_hook_order_test/src/Hook/DAlterHooks.php b/core/modules/system/tests/modules/HookOrder/ddd_hook_order_test/src/Hook/DAlterHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..7da7d91002cd7e3f5d7dbd372837134a5c37de6e --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/ddd_hook_order_test/src/Hook/DAlterHooks.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\ddd_hook_order_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. + */ +class DAlterHooks { + + #[Hook('test_alter')] + public function testAlter(array &$calls): void { + $calls[] = __METHOD__; + } + + #[Hook('test_subtype_alter')] + public function testSubtypeAlter(array &$calls): void { + $calls[] = __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/HookOrder/ddd_hook_order_test/src/Hook/DHooks.php b/core/modules/system/tests/modules/HookOrder/ddd_hook_order_test/src/Hook/DHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..053228642d6474e4b14bc8da23f273613248a827 --- /dev/null +++ b/core/modules/system/tests/modules/HookOrder/ddd_hook_order_test/src/Hook/DHooks.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\ddd_hook_order_test\Hook; + +use Drupal\Core\Hook\Attribute\Hook; +use Drupal\Core\Hook\Attribute\RemoveHook; +use Drupal\Core\Hook\Attribute\ReorderHook; +use Drupal\Core\Hook\Order\Order; +use Drupal\ccc_hook_order_test\Hook\CHooks; + +/** + * This class contains hook implementations. + * + * By default, these will be called in module order, which is predictable due + * to the alphabetical module names. + */ +#[ReorderHook('test_hook', CHooks::class, 'testHookReorderFirst', Order::First)] +#[RemoveHook('test_hook', CHooks::class, 'testHookRemoved')] +class DHooks { + + #[Hook('test_hook')] + public function testHook(): string { + return __METHOD__; + } + + #[Hook('sparse_test_hook')] + public function sparseTestHook(): string { + return __METHOD__; + } + +} 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 4e978472953ba50b82fa69c6e4337d4d6f0a94cd..5d7fdc3dc6b8d0150d2127d379d7ef0d5db77430 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_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..3d4e53eba0bbd0418cbdfc627ab38ab87fa47434 --- /dev/null +++ b/core/modules/system/tests/modules/hook_test_remove/src/Hook/TestHookRemove.php @@ -0,0 +1,38 @@ +<?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 function hookDoNotRun(): string { + // This hook should not run. + return __METHOD__; + } + + /** + * This hook should run and prevent custom_hook1. + */ + #[Hook('custom_hook1')] + #[RemoveHook( + 'custom_hook1', + class: TestHookRemove::class, + method: 'hookDoNotRun' + )] + public function hookDoRun(): string { + // This hook should run. + return __METHOD__; + } + +} diff --git a/core/modules/system/tests/modules/module_test/module_test.implementations.inc b/core/modules/system/tests/modules/module_implements_alter_test/module_implements_alter_test.implementations.inc similarity index 54% rename from core/modules/system/tests/modules/module_test/module_test.implementations.inc rename to core/modules/system/tests/modules/module_implements_alter_test/module_implements_alter_test.implementations.inc index b971af58c8c1781b2ee9e0c53eceacc4a49b4c49..7b6bac4ae95dc2d23564c4b807dfa352a3e263be 100644 --- a/core/modules/system/tests/modules/module_test/module_test.implementations.inc +++ b/core/modules/system/tests/modules/module_implements_alter_test/module_implements_alter_test.implementations.inc @@ -10,8 +10,8 @@ /** * Implements hook_altered_test_hook(). * - * @see module_test_module_implements_alter() + * @see module_implements_alter_test_module_implements_alter() */ -function module_test_altered_test_hook(): string { +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..c379d30c360d20279deb00a4c1e88a016f82a02b --- /dev/null +++ b/core/modules/system/tests/modules/module_implements_alter_test/module_implements_alter_test.module @@ -0,0 +1,41 @@ +<?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/workspaces/src/Hook/EntityOperations.php b/core/modules/workspaces/src/Hook/EntityOperations.php index c459f4d065e3fbd47998ffa6941a561452993034..377ea62b2e88aae74310b3dc28e3bca43d05971a 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\Order; +use Drupal\Core\Hook\Order\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..2a8ad684b5385ed25235d2ba48fa047d42ec951f 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,32 @@ 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()); - - $this->assertTrue(\Drupal::moduleHandler()->hasImplementations('modules_installed', 'module_test'), - 'module_test implements hook_modules_installed().'); + // 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('module_implements_alter', 'module_test'), - 'module_test implements hook_module_implements_alter().'); + $this->assertTrue(\Drupal::moduleHandler()->hasImplementations('module_implements_alter', 'module_implements_alter_test'), + 'module_implements_alter_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_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.'); + // 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.'); } } diff --git a/core/tests/Drupal/KernelTests/Core/Hook/HookAlterOrderTest.php b/core/tests/Drupal/KernelTests/Core/Hook/HookAlterOrderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a96040e903569839914b684694fcce65d0489f73 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Hook/HookAlterOrderTest.php @@ -0,0 +1,226 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Hook; + +use Drupal\aaa_hook_order_test\Hook\AAlterHooks; +use Drupal\aaa_hook_order_test\Hook\ModuleImplementsAlter; +use Drupal\bbb_hook_order_test\Hook\BAlterHooks; +use Drupal\ccc_hook_order_test\Hook\CAlterHooks; +use Drupal\ddd_hook_order_test\Hook\DAlterHooks; +use Drupal\KernelTests\KernelTestBase; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; + +/** + * @group Hook + */ +#[IgnoreDeprecations] +class HookAlterOrderTest extends KernelTestBase { + + use HookOrderTestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'aaa_hook_order_test', + 'bbb_hook_order_test', + 'ccc_hook_order_test', + 'ddd_hook_order_test', + ]; + + public function testProceduralModuleImplementsAlterOrder(): void { + $this->assertAlterCallOrder($main_unaltered = [ + 'aaa_hook_order_test_procedural_alter', + 'bbb_hook_order_test_procedural_alter', + 'ccc_hook_order_test_procedural_alter', + ], 'procedural'); + + $this->assertAlterCallOrder($sub_unaltered = [ + 'aaa_hook_order_test_procedural_subtype_alter', + 'bbb_hook_order_test_procedural_subtype_alter', + 'ccc_hook_order_test_procedural_subtype_alter', + ], 'procedural_subtype'); + + $this->assertAlterCallOrder($combined_unaltered = [ + 'aaa_hook_order_test_procedural_alter', + 'aaa_hook_order_test_procedural_subtype_alter', + 'bbb_hook_order_test_procedural_alter', + 'bbb_hook_order_test_procedural_subtype_alter', + 'ccc_hook_order_test_procedural_alter', + 'ccc_hook_order_test_procedural_subtype_alter', + ], ['procedural', 'procedural_subtype']); + + $move_b_down = function (array &$implementations): void { + // Move B to the end, no matter which hook. + $group = $implementations['bbb_hook_order_test']; + unset($implementations['bbb_hook_order_test']); + $implementations['bbb_hook_order_test'] = $group; + }; + $modules = ['aaa_hook_order_test', 'bbb_hook_order_test', 'ccc_hook_order_test']; + + // Test with module B moved to the end for both hooks. + ModuleImplementsAlter::set( + function (array &$implementations, string $hook) use ($modules, $move_b_down): void { + if (!in_array($hook, ['procedural_alter', 'procedural_subtype_alter'])) { + return; + } + $this->assertSame($modules, array_keys($implementations)); + $move_b_down($implementations); + }, + ); + \Drupal::service('kernel')->rebuildContainer(); + + $this->assertAlterCallOrder($main_altered = [ + 'aaa_hook_order_test_procedural_alter', + 'ccc_hook_order_test_procedural_alter', + // The implementation of B has been moved. + 'bbb_hook_order_test_procedural_alter', + ], 'procedural'); + + $this->assertAlterCallOrder($sub_altered = [ + 'aaa_hook_order_test_procedural_subtype_alter', + 'ccc_hook_order_test_procedural_subtype_alter', + // The implementation of B has been moved. + 'bbb_hook_order_test_procedural_subtype_alter', + ], 'procedural_subtype'); + + $this->assertAlterCallOrder($combined_altered = [ + 'aaa_hook_order_test_procedural_alter', + 'aaa_hook_order_test_procedural_subtype_alter', + 'ccc_hook_order_test_procedural_alter', + 'ccc_hook_order_test_procedural_subtype_alter', + // The implementation of B has been moved. + 'bbb_hook_order_test_procedural_alter', + 'bbb_hook_order_test_procedural_subtype_alter', + ], ['procedural', 'procedural_subtype']); + + // If the altered hook is not the first one, implementations are back in + // their unaltered order. + $this->assertAlterCallOrder($main_unaltered, ['other_main_type', 'procedural']); + $this->assertAlterCallOrder($sub_unaltered, ['other_main_type', 'procedural_subtype']); + $this->assertAlterCallOrder($combined_unaltered, ['other_main_type', 'procedural', 'procedural_subtype']); + + // Test with module B moved to the end for the main hook. + ModuleImplementsAlter::set( + function (array &$implementations, string $hook) use ($modules, $move_b_down): void { + if (!in_array($hook, ['procedural_alter', 'procedural_subtype_alter'])) { + return; + } + $this->assertSame($modules, array_keys($implementations)); + if ($hook !== 'procedural_alter') { + return; + } + $move_b_down($implementations); + }, + ); + \Drupal::service('kernel')->rebuildContainer(); + + $this->assertAlterCallOrder($main_altered, 'procedural'); + $this->assertAlterCallOrder($sub_unaltered, 'procedural_subtype'); + $this->assertAlterCallOrder($combined_altered, ['procedural', 'procedural_subtype']); + + // Test with module B moved to the end for the subtype hook. + ModuleImplementsAlter::set( + function (array &$implementations, string $hook) use ($modules, $move_b_down): void { + if (!in_array($hook, ['procedural_alter', 'procedural_subtype_alter'])) { + return; + } + $this->assertSameCallList($modules, array_keys($implementations)); + if ($hook !== 'procedural_subtype_alter') { + return; + } + $move_b_down($implementations); + }, + ); + \Drupal::service('kernel')->rebuildContainer(); + + $this->assertAlterCallOrder($main_unaltered, 'procedural'); + $this->assertAlterCallOrder($sub_altered, 'procedural_subtype'); + $this->assertAlterCallOrder($combined_unaltered, ['procedural', 'procedural_subtype']); + } + + public function testAlterOrder(): void { + $this->assertAlterCallOrder([ + CAlterHooks::class . '::testAlter', + AAlterHooks::class . '::testAlterAfterC', + DAlterHooks::class . '::testAlter', + ], 'test'); + + $this->assertAlterCallOrder([ + AAlterHooks::class . '::testSubtypeAlter', + BAlterHooks::class . '::testSubtypeAlter', + CAlterHooks::class . '::testSubtypeAlter', + DAlterHooks::class . '::testSubtypeAlter', + ], 'test_subtype'); + + $this->assertAlterCallOrder([ + // The implementation from 'D' is gone. + AAlterHooks::class . '::testSubtypeAlter', + BAlterHooks::class . '::testSubtypeAlter', + CAlterHooks::class . '::testAlter', + CAlterHooks::class . '::testSubtypeAlter', + AAlterHooks::class . '::testAlterAfterC', + DAlterHooks::class . '::testAlter', + DAlterHooks::class . '::testSubtypeAlter', + ], ['test', 'test_subtype']); + + $this->disableModules(['bbb_hook_order_test']); + + $this->assertAlterCallOrder([ + CAlterHooks::class . '::testAlter', + AAlterHooks::class . '::testAlterAfterC', + DAlterHooks::class . '::testAlter', + ], 'test'); + + $this->assertAlterCallOrder([ + AAlterHooks::class . '::testSubtypeAlter', + CAlterHooks::class . '::testSubtypeAlter', + DAlterHooks::class . '::testSubtypeAlter', + ], 'test_subtype'); + + $this->assertAlterCallOrder([ + AAlterHooks::class . '::testSubtypeAlter', + CAlterHooks::class . '::testAlter', + CAlterHooks::class . '::testSubtypeAlter', + AAlterHooks::class . '::testAlterAfterC', + DAlterHooks::class . '::testAlter', + DAlterHooks::class . '::testSubtypeAlter', + ], ['test', 'test_subtype']); + } + + /** + * Asserts the call order from an alter call. + * + * Also asserts additional $type argument values that are meant to produce the + * same result. + * + * @param list<string> $expected + * Expected call list, as strings from __METHOD__ or __FUNCTION__. + * @param string|list<string> $type + * First argument to pass to ->alter(). + */ + protected function assertAlterCallOrder(array $expected, string|array $type): void { + $this->assertSameCallList( + $expected, + $this->alter($type), + ); + } + + /** + * Invokes ModuleHandler->alter() and returns the altered array. + * + * @param string|list<string> $type + * Alter type or list of alter types. + * + * @return array + * The altered array. + */ + protected function alter(string|array $type): array { + $data = []; + \Drupal::moduleHandler()->alter($type, $data); + return $data; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php b/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php index 4156481d3b924a88d8fcf1b0953b80e1669b66c2..774f1289ccdb95ec8ccca89a9d45967eac02d6c8 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,150 @@ 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'); + $module_installer->install(['aaa_hook_collector_test']); + $module_installer->install(['bbb_hook_collector_test']); + $module_handler = $this->container->get('module_handler'); + // Last alphabetically uses the Order::First enum to place it before + // the implementation it would naturally come after. + $expected_calls = [ + 'Drupal\bbb_hook_collector_test\Hook\TestHookFirst::hookFirst', + 'Drupal\aaa_hook_collector_test\Hook\TestHookFirst::hookFirst', + ]; + $calls = $module_handler->invokeAll('custom_hook_test_hook_first'); + $this->assertEquals($expected_calls, $calls); + } + + /** + * Tests hook ordering with attributes. + */ + public function testHookAfter(): void { + $module_installer = $this->container->get('module_installer'); + $module_installer->install(['aaa_hook_collector_test']); + $module_installer->install(['bbb_hook_collector_test']); + $module_handler = $this->container->get('module_handler'); + // First alphabetically uses the OrderAfter to place it after + // the implementation it would naturally come before. + $expected_calls = [ + 'Drupal\bbb_hook_collector_test\Hook\TestHookAfter::hookAfter', + 'Drupal\aaa_hook_collector_test\Hook\TestHookAfter::hookAfter', + ]; + $calls = $module_handler->invokeAll('custom_hook_test_hook_after'); + $this->assertEquals($expected_calls, $calls); + } + + /** + * Tests hook ordering with attributes. + */ + public function testHookAfterClassMethod(): void { + $module_installer = $this->container->get('module_installer'); + $module_installer->install(['aaa_hook_collector_test']); + $module_installer->install(['bbb_hook_collector_test']); + $module_handler = $this->container->get('module_handler'); + // First alphabetically uses the OrderAfter to place it after + // the implementation it would naturally come before using call and method. + $expected_calls = [ + 'Drupal\bbb_hook_collector_test\Hook\TestHookAfterClassMethod::hookAfterClassMethod', + 'Drupal\aaa_hook_collector_test\Hook\TestHookAfterClassMethod::hookAfterClassMethod', + ]; + $calls = $module_handler->invokeAll('custom_hook_test_hook_after_class_method'); + $this->assertEquals($expected_calls, $calls); + } + + /** + * Tests hook ordering with attributes. + */ + public function testHookBefore(): void { + $module_installer = $this->container->get('module_installer'); + $module_installer->install(['aaa_hook_collector_test']); + $module_installer->install(['bbb_hook_collector_test']); + $module_handler = $this->container->get('module_handler'); + // First alphabetically uses the OrderBefore to place it before + // the implementation it would naturally come after. + $expected_calls = [ + 'Drupal\bbb_hook_collector_test\Hook\TestHookBefore::hookBefore', + 'Drupal\aaa_hook_collector_test\Hook\TestHookBefore::hookBefore', + ]; + $calls = $module_handler->invokeAll('custom_hook_test_hook_before'); + $this->assertEquals($expected_calls, $calls); + } + + /** + * Tests hook ordering with attributes. + */ + public function testHookOrderExtraTypes(): void { + $module_installer = $this->container->get('module_installer'); + $module_installer->install(['aaa_hook_collector_test']); + $module_installer->install(['bbb_hook_collector_test']); + $module_handler = $this->container->get('module_handler'); + // First alphabetically uses the OrderAfter to place it after + // the implementation it would naturally come before. + $expected_calls = [ + 'Drupal\bbb_hook_collector_test\Hook\TestHookOrderExtraTypes::customHookExtraTypes', + 'Drupal\aaa_hook_collector_test\Hook\TestHookOrderExtraTypes::customHookExtraTypes', + ]; + $hooks = [ + 'custom_hook', + 'custom_hook_extra_types1', + 'custom_hook_extra_types2', + ]; + $calls = []; + $module_handler->alter($hooks, $calls); + $this->assertEquals($expected_calls, $calls); + } + + /** + * Tests hook ordering with attributes. + */ + public function testHookLast(): void { + $module_installer = $this->container->get('module_installer'); + $module_installer->install(['aaa_hook_collector_test']); + $module_installer->install(['bbb_hook_collector_test']); + $module_handler = $this->container->get('module_handler'); + // First alphabetically uses the OrderBefore to place it before + // the implementation it would naturally come after. + $expected_calls = [ + 'Drupal\bbb_hook_collector_test\Hook\TestHookLast::hookLast', + 'Drupal\aaa_hook_collector_test\Hook\TestHookLast::hookLast', + ]; + $calls = $module_handler->invokeAll('custom_hook_test_hook_last'); + $this->assertEquals($expected_calls, $calls); + } + + /** + * Tests hook remove. + */ + public function testHookRemove(): void { + $module_installer = $this->container->get('module_installer'); + $this->assertTrue($module_installer->install(['hook_test_remove'])); + $module_handler = $this->container->get('module_handler'); + // There are two hooks implementing custom_hook1. + // One is removed with RemoveHook so it should not run. + $expected_calls = [ + 'Drupal\hook_test_remove\Hook\TestHookRemove::hookDoRun', + ]; + $calls = $module_handler->invokeAll('custom_hook1'); + $this->assertEquals($expected_calls, $calls); + } + + /** + * Tests hook override. + */ + public function testHookOverride(): void { + $module_installer = $this->container->get('module_installer'); + $module_installer->install(['aaa_hook_collector_test']); + $module_installer->install(['bbb_hook_collector_test']); + $module_handler = $this->container->get('module_handler'); + $expected_calls = [ + 'Drupal\aaa_hook_collector_test\Hook\TestHookReorderHookFirst::customHookOverride', + 'Drupal\bbb_hook_collector_test\Hook\TestHookReorderHookLast::customHookOverride', + ]; + $calls = $module_handler->invokeAll('custom_hook_override'); + $this->assertEquals($expected_calls, $calls); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Hook/HookOrderTest.php b/core/tests/Drupal/KernelTests/Core/Hook/HookOrderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..971c95bc8e9ddd46390640a1563ce37e8a350666 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Hook/HookOrderTest.php @@ -0,0 +1,74 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Hook; + +use Drupal\aaa_hook_order_test\Hook\AHooks; +use Drupal\bbb_hook_order_test\Hook\BHooks; +use Drupal\ccc_hook_order_test\Hook\CHooks; +use Drupal\ddd_hook_order_test\Hook\DHooks; +use Drupal\KernelTests\KernelTestBase; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; + +/** + * @group Hook + */ +#[IgnoreDeprecations] +class HookOrderTest extends KernelTestBase { + + use HookOrderTestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'aaa_hook_order_test', + 'bbb_hook_order_test', + 'ccc_hook_order_test', + 'ddd_hook_order_test', + ]; + + public function testHookOrder(): void { + $this->assertSameCallList( + [ + CHooks::class . '::testHookReorderFirst', + CHooks::class . '::testHookFirst', + AHooks::class . '::testHookFirst', + 'aaa_hook_order_test_test_hook', + AHooks::class . '::testHook', + 'bbb_hook_order_test_test_hook', + BHooks::class . '::testHook', + AHooks::class . '::testHookAfterB', + 'ccc_hook_order_test_test_hook', + CHooks::class . '::testHook', + 'ddd_hook_order_test_test_hook', + DHooks::class . '::testHook', + AHooks::class . '::testHookLast', + ], + \Drupal::moduleHandler()->invokeAll('test_hook'), + ); + } + + /** + * Tests hook order when each module has either oop or procedural listeners. + * + * This would detect a possible mistake where we would first collect modules + * from all procedural and then from all oop implementations, without fixing + * the order. + */ + public function testSparseHookOrder(): void { + $this->assertSameCallList( + [ + // OOP and procedural listeners are correctly intermixed by module + // order. + 'aaa_hook_order_test_sparse_test_hook', + BHooks::class . '::sparseTestHook', + 'ccc_hook_order_test_sparse_test_hook', + DHooks::class . '::sparseTestHook', + ], + \Drupal::moduleHandler()->invokeAll('sparse_test_hook'), + ); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Hook/HookOrderTestTrait.php b/core/tests/Drupal/KernelTests/Core/Hook/HookOrderTestTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..15238c7b33c7ae0b732fc86e50819cd22f5f3edb --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Hook/HookOrderTestTrait.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Hook; + +/** + * @group Hook + */ +trait HookOrderTestTrait { + + /** + * Asserts that two lists of call strings are the same. + * + * It is meant for strings produced with __FUNCTION__ or __METHOD__. + * + * The assertion fails exactly when a regular ->assertSame() would fail, but + * it provides a more useful output on failure. + * + * @param list<string> $expected + * Expected list of strings. + * @param list<string> $actual + * Actual list of strings. + * @param string $message + * Message to pass to ->assertSame(). + */ + protected function assertSameCallList(array $expected, array $actual, string $message = ''): void { + // Format without the numeric array keys, but in a way that can be easily + // copied into the test. + $format = function (array $strings): string { + if (!$strings) { + return '[]'; + } + $parts = array_map( + static function (string $call_string) { + if (preg_match('@^(\w+\\\\)*(\w+)::(\w+)@', $call_string, $matches)) { + [,, $class_shortname, $method] = $matches; + return $class_shortname . '::class . ' . var_export('::' . $method, TRUE); + } + return var_export($call_string, TRUE); + }, + $strings, + ); + return "[\n " . implode(",\n ", $parts) . ",\n]"; + }; + $this->assertSame( + $format($expected), + $format($actual), + $message, + ); + // Finally, assert that array keys and the full class names are really the + // same, in a way that provides useful output on failure. + $this->assertSame($expected, $actual, $message); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerTest.php b/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerTest.php index 3b36018806f74ce056163767a47ef712d9d28788..05a064eab7dfc9dae959613390f5275112d6f9c5 100644 --- a/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerTest.php @@ -102,6 +102,8 @@ public function testLoadModule(): void { * Tests loading all modules. * * @covers ::loadAll + * + * @group legacy */ public function testLoadAllModules(): void { $moduleList = [ @@ -352,6 +354,8 @@ public function testImplementsHookModuleEnabled(): void { * Tests invoke all. * * @covers ::invokeAll + * + * @group legacy */ public function testInvokeAll(): void { $implementations = [ @@ -382,7 +386,7 @@ function some_method(): void { }; $implementations['some_hook'][get_class($c)]['some_method'] = 'some_module'; - $module_handler = new ModuleHandler($this->root, [], $this->eventDispatcher, $implementations, []); + $module_handler = new ModuleHandler($this->root, [], $this->eventDispatcher, $implementations); $module_handler->setModuleList(['some_module' => TRUE]); $r = new \ReflectionObject($module_handler); 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)); - } - }