summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--core/core.api.php3
-rw-r--r--core/core.services.yml2
-rw-r--r--core/lib/Drupal/Core/Hook/Attribute/Hook.php28
-rw-r--r--core/lib/Drupal/Core/Hook/Attribute/Preprocess.php23
-rw-r--r--core/lib/Drupal/Core/Hook/Attribute/ProceduralHookScanStop.php23
-rw-r--r--core/lib/Drupal/Core/Hook/Attribute/StopProceduralHookScan.php16
-rw-r--r--core/lib/Drupal/Core/Hook/HookCollectorPass.php24
-rw-r--r--core/lib/Drupal/Core/Theme/Registry.php130
-rw-r--r--core/lib/Drupal/Core/Theme/ThemeManager.php13
-rw-r--r--core/lib/Drupal/Core/Utility/ThemeRegistry.php12
-rw-r--r--core/modules/announcements_feed/announcements_feed.services.yml2
-rw-r--r--core/modules/automated_cron/automated_cron.services.yml2
-rw-r--r--core/modules/ban/ban.services.yml2
-rw-r--r--core/modules/basic_auth/basic_auth.services.yml2
-rw-r--r--core/modules/block_content/block_content.services.yml2
-rw-r--r--core/modules/breakpoint/breakpoint.services.yml2
-rw-r--r--core/modules/comment/comment.module9
-rw-r--r--core/modules/comment/comment.services.yml2
-rw-r--r--core/modules/comment/src/Hook/CommentThemeHooks.php22
-rw-r--r--core/modules/comment/tests/modules/comment_empty_title_test/comment_empty_title_test.module15
-rw-r--r--core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php22
-rw-r--r--core/modules/config/config.services.yml2
-rw-r--r--core/modules/config_translation/config_translation.services.yml2
-rw-r--r--core/modules/contact/contact.services.yml2
-rw-r--r--core/modules/content_moderation/content_moderation.services.yml2
-rw-r--r--core/modules/contextual/contextual.module40
-rw-r--r--core/modules/contextual/contextual.services.yml2
-rw-r--r--core/modules/contextual/src/Hook/ContextualThemeHooks.php57
-rw-r--r--core/modules/datetime/datetime.services.yml2
-rw-r--r--core/modules/datetime_range/datetime_range.services.yml2
-rw-r--r--core/modules/dblog/dblog.services.yml2
-rw-r--r--core/modules/dynamic_page_cache/dynamic_page_cache.services.yml2
-rw-r--r--core/modules/editor/editor.services.yml2
-rw-r--r--core/modules/field/field.services.yml2
-rw-r--r--core/modules/field_layout/field_layout.services.yml2
-rw-r--r--core/modules/field_ui/field_ui.services.yml2
-rw-r--r--core/modules/filter/filter.services.yml2
-rw-r--r--core/modules/help/help.services.yml2
-rw-r--r--core/modules/history/history.services.yml2
-rw-r--r--core/modules/inline_form_errors/inline_form_errors.services.yml2
-rw-r--r--core/modules/language/language.services.yml2
-rw-r--r--core/modules/link/link.services.yml2
-rw-r--r--core/modules/locale/locale.batch.inc4
-rw-r--r--core/modules/locale/locale.bulk.inc4
-rw-r--r--core/modules/locale/locale.compare.inc4
-rw-r--r--core/modules/locale/locale.fetch.inc4
-rw-r--r--core/modules/locale/locale.install4
-rw-r--r--core/modules/locale/locale.module29
-rw-r--r--core/modules/locale/locale.pages.inc4
-rw-r--r--core/modules/locale/locale.translation.inc4
-rw-r--r--core/modules/locale/src/Hook/LocaleThemeHooks.php43
-rw-r--r--core/modules/media/media.install4
-rw-r--r--core/modules/media/media.module4
-rw-r--r--core/modules/media_library/media_library.services.yml2
-rw-r--r--core/modules/menu_link_content/menu_link_content.services.yml2
-rw-r--r--core/modules/menu_ui/menu_ui.services.yml2
-rw-r--r--core/modules/migrate/migrate.services.yml2
-rw-r--r--core/modules/migrate_drupal/migrate_drupal.services.yml2
-rw-r--r--core/modules/migrate_drupal_ui/migrate_drupal_ui.services.yml2
-rw-r--r--core/modules/node/node.module24
-rw-r--r--core/modules/node/src/Hook/NodeThemeHooks.php38
-rw-r--r--core/modules/options/options.services.yml2
-rw-r--r--core/modules/path/path.services.yml2
-rw-r--r--core/modules/responsive_image/responsive_image.services.yml2
-rw-r--r--core/modules/rest/rest.services.yml2
-rw-r--r--core/modules/serialization/serialization.services.yml2
-rw-r--r--core/modules/settings_tray/settings_tray.services.yml2
-rw-r--r--core/modules/shortcut/shortcut.services.yml2
-rw-r--r--core/modules/syslog/syslog.services.yml2
-rw-r--r--core/modules/system/tests/modules/hook_collector_skip_procedural/hook_collector_skip_procedural.services.yml2
-rw-r--r--core/modules/system/tests/modules/hook_collector_skip_procedural_attribute/hook_collector_skip_procedural_attribute.module4
-rw-r--r--core/modules/system/tests/modules/module_test_oop_preprocess/module_test_oop_preprocess.info.yml5
-rw-r--r--core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php24
-rw-r--r--core/modules/system/tests/modules/module_test_procedural_preprocess/module_test_procedural_preprocess.info.yml5
-rw-r--r--core/modules/system/tests/modules/module_test_procedural_preprocess/module_test_procedural_preprocess.module20
-rw-r--r--core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php22
-rw-r--r--core/modules/system/tests/modules/theme_test/theme_test.module7
-rw-r--r--core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php50
-rw-r--r--core/modules/text/text.services.yml2
-rw-r--r--core/modules/toolbar/toolbar.services.yml2
-rw-r--r--core/modules/update/update.authorize.inc4
-rw-r--r--core/modules/update/update.compare.inc4
-rw-r--r--core/modules/update/update.fetch.inc4
-rw-r--r--core/modules/update/update.install4
-rw-r--r--core/modules/update/update.manager.inc4
-rw-r--r--core/modules/update/update.module4
-rw-r--r--core/modules/update/update.report.inc2
-rw-r--r--core/modules/workflows/workflows.services.yml2
-rw-r--r--core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php13
-rw-r--r--core/tests/Drupal/Tests/Core/Theme/RegistryTest.php18
90 files changed, 624 insertions, 267 deletions
diff --git a/core/core.api.php b/core/core.api.php
index e93fa0bb5b5c..23c0ef741c43 100644
--- a/core/core.api.php
+++ b/core/core.api.php
@@ -1677,8 +1677,7 @@
* - hook_update_last_removed()
* - hook_update_N()
*
- * Theme hooks:
- * - hook_preprocess_HOOK()
+ * Hooks implemented by themes must remain procedural.
*
* @subsection procedural-hooks Procedural hook implementation
*
diff --git a/core/core.services.yml b/core/core.services.yml
index 4372e6512c3e..a337173741f7 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1564,7 +1564,7 @@ services:
Drupal\Core\Theme\ThemeInitializationInterface: '@theme.initialization'
theme.registry:
class: Drupal\Core\Theme\Registry
- arguments: ['%app.root%', '@cache.default', '@lock', '@module_handler', '@theme_handler', '@theme.initialization', '@cache.bootstrap', '@extension.list.module', '@kernel']
+ arguments: ['%app.root%', '@cache.default', '@lock', '@module_handler', '@theme_handler', '@theme.initialization', '@cache.bootstrap', '@extension.list.module', '@kernel', ~, '%preprocess_for_suggestions%']
tags:
- { name: needs_destruction }
calls:
diff --git a/core/lib/Drupal/Core/Hook/Attribute/Hook.php b/core/lib/Drupal/Core/Hook/Attribute/Hook.php
index 33da9558b512..1651e1578f7b 100644
--- a/core/lib/Drupal/Core/Hook/Attribute/Hook.php
+++ b/core/lib/Drupal/Core/Hook/Attribute/Hook.php
@@ -83,8 +83,7 @@ use Drupal\Core\Hook\Order\OrderInterface;
* - hook_update_last_removed()
* - hook_update_N()
*
- * Theme hooks:
- * - hook_preprocess_HOOK()
+ * Hooks implemented by themes must remain procedural.
*
* @section sec_backwards_compatibility Backwards-compatibility
*
@@ -97,12 +96,28 @@ use Drupal\Core\Hook\Order\OrderInterface;
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class Hook implements HookAttributeInterface {
+ /**
+ * The hook prefix such as `form`.
+ *
+ * @var string
+ */
+ public const string PREFIX = '';
+
+ /**
+ * The hook suffix such as `alter`.
+ *
+ * @var string
+ */
+ public const string SUFFIX = '';
/**
* Constructs a Hook attribute object.
*
* @param string $hook
* The short hook name, without the 'hook_' prefix.
+ * $hook is only optional when Hook is extended and a PREFIX or SUFFIX is
+ * defined. When using the [#Hook] attribute directly $hook is required.
+ * See Drupal\Core\Hook\Attribute\Preprocess.
* @param string $method
* (optional) The method name. If this attribute is on a method, this
* parameter is not required. If this attribute is on a class and this
@@ -116,10 +131,15 @@ class Hook implements HookAttributeInterface {
* (optional) Set the order of the implementation.
*/
public function __construct(
- public string $hook,
+ public string $hook = '',
public string $method = '',
public ?string $module = NULL,
public OrderInterface|null $order = NULL,
- ) {}
+ ) {
+ $this->hook = implode('_', array_filter([static::PREFIX, $hook, static::SUFFIX]));
+ if ($this->hook === '') {
+ throw new \LogicException('The Hook attribute or an attribute extending the Hook attribute must provide the $hook parameter, a PREFIX or a SUFFIX.');
+ }
+ }
}
diff --git a/core/lib/Drupal/Core/Hook/Attribute/Preprocess.php b/core/lib/Drupal/Core/Hook/Attribute/Preprocess.php
new file mode 100644
index 000000000000..47642859a20b
--- /dev/null
+++ b/core/lib/Drupal/Core/Hook/Attribute/Preprocess.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Core\Hook\Attribute;
+
+/**
+ * Attribute for defining a class method as a preprocess function.
+ *
+ * Pass no arguments for hook_preprocess `#[Preprocess]`.
+ * For `hook_preprocess_HOOK` pass the `HOOK` without the `hook_preprocess`
+ * portion `#[Preprocess('HOOK')]`.
+ *
+ * See \Drupal\Core\Hook\Attribute\Hook for additional information.
+ */
+#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
+class Preprocess extends Hook {
+ /**
+ * {@inheritdoc}
+ */
+ public const string PREFIX = 'preprocess';
+
+}
diff --git a/core/lib/Drupal/Core/Hook/Attribute/ProceduralHookScanStop.php b/core/lib/Drupal/Core/Hook/Attribute/ProceduralHookScanStop.php
new file mode 100644
index 000000000000..a030b750262b
--- /dev/null
+++ b/core/lib/Drupal/Core/Hook/Attribute/ProceduralHookScanStop.php
@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Core\Hook\Attribute;
+
+/**
+ * Defines a ProceduralHookScanStop attribute object.
+ *
+ * This allows contrib and core to mark when a file has no more
+ * procedural hooks to be gathered. Any procedural hooks in the file should
+ * be placed before the function with this attribute. This includes all hooks
+ * that can be converted to object oriented hooks and also includes:
+ * - hook_hook_info()
+ * - hook_module_implements_alter()
+ * - hook_requirements()
+ * - hook_preprocess()
+ * - hook_preprocess_HOOK()
+ */
+#[\Attribute(\Attribute::TARGET_FUNCTION)]
+class ProceduralHookScanStop {
+
+}
diff --git a/core/lib/Drupal/Core/Hook/Attribute/StopProceduralHookScan.php b/core/lib/Drupal/Core/Hook/Attribute/StopProceduralHookScan.php
deleted file mode 100644
index 73f0ce6915bd..000000000000
--- a/core/lib/Drupal/Core/Hook/Attribute/StopProceduralHookScan.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Drupal\Core\Hook\Attribute;
-
-/**
- * Defines a StopProceduralHookScan attribute object.
- *
- * This allows contrib and core to mark when a file has no more
- * procedural hooks.
- */
-#[\Attribute(\Attribute::TARGET_FUNCTION)]
-class StopProceduralHookScan {
-
-}
diff --git a/core/lib/Drupal/Core/Hook/HookCollectorPass.php b/core/lib/Drupal/Core/Hook/HookCollectorPass.php
index 3fe2a6d28307..7c0f913ca219 100644
--- a/core/lib/Drupal/Core/Hook/HookCollectorPass.php
+++ b/core/lib/Drupal/Core/Hook/HookCollectorPass.php
@@ -14,7 +14,7 @@ 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\Attribute\ProceduralHookScanStop;
use Drupal\Core\Hook\OrderOperation\OrderOperation;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -97,6 +97,16 @@ class HookCollectorPass implements CompilerPassInterface {
private array $hookInfo = [];
/**
+ * Preprocess suggestions discovered in modules.
+ *
+ * These are stored to prevent adding preprocess suggestions to the invoke map
+ * that are not discovered in modules.
+ *
+ * @var array<string, true>
+ */
+ protected array $preprocessForSuggestions;
+
+ /**
* Include files, keyed by the $group part of "/$module.$group.inc".
*
* @var array<string, list<string>>
@@ -122,7 +132,7 @@ class HookCollectorPass implements CompilerPassInterface {
$parameters = $container->getParameterBag()->all();
$skip_procedural_modules = array_filter(
array_keys($module_list),
- static fn (string $module) => !empty($parameters["$module.hooks_converted"]),
+ static fn (string $module) => !empty($parameters["$module.skip_procedural_hook_scan"]),
);
$collector = static::collectAllHookImplementations($module_list, $skip_procedural_modules);
@@ -154,6 +164,7 @@ class HookCollectorPass implements CompilerPassInterface {
$implementationsByHook = $this->calculateImplementations();
static::writeImplementationsToContainer($container, $implementationsByHook);
+ $container->setParameter('preprocess_for_suggestions', $this->preprocessForSuggestions ?? []);
// Update the module handler definition.
$definition = $container->getDefinition('module_handler');
@@ -228,6 +239,9 @@ class HookCollectorPass implements CompilerPassInterface {
$alter($moduleImplements, $hook);
}
foreach ($moduleImplements as $module => $v) {
+ if (is_string($hook) && str_starts_with($hook, 'preprocess_') && str_contains($hook, '__')) {
+ $this->preprocessForSuggestions[$module . '_' . $hook] = TRUE;
+ }
foreach (array_keys($implementationsByHookOrig[$hook], $module, TRUE) as $identifier) {
$implementationsByHook[$hook][$identifier] = $module;
}
@@ -354,7 +368,7 @@ class HookCollectorPass implements CompilerPassInterface {
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]+$))/';
+ $module_preg = '/^(?<function>(?<module>' . $known_modules_pattern . ')_(?!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);
@@ -441,7 +455,7 @@ class HookCollectorPass implements CompilerPassInterface {
$parser = new StaticReflectionParser('', $finder);
$implementations = [];
foreach ($parser->getMethodAttributes() as $function => $attributes) {
- if (StaticReflectionParser::hasAttribute($attributes, StopProceduralHookScan::class)) {
+ if (StaticReflectionParser::hasAttribute($attributes, ProceduralHookScanStop::class)) {
break;
}
if (!StaticReflectionParser::hasAttribute($attributes, LegacyHook::class) && preg_match($module_preg, $function, $matches) && !StaticReflectionParser::hasAttribute($attributes, LegacyModuleImplementsAlter::class)) {
@@ -572,7 +586,7 @@ class HookCollectorPass implements CompilerPassInterface {
'install_tasks_alter',
];
- if (in_array($hookAttribute->hook, $staticDenyHooks) || preg_match('/^(post_update_|preprocess_|update_\d+$)/', $hookAttribute->hook)) {
+ if (in_array($hookAttribute->hook, $staticDenyHooks) || preg_match('/^(post_update_|update_\d+$)/', $hookAttribute->hook)) {
throw new \LogicException("The hook $hookAttribute->hook on class $class does not support attributes and must remain procedural.");
}
}
diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php
index 2317cedb91c3..383776b65be8 100644
--- a/core/lib/Drupal/Core/Theme/Registry.php
+++ b/core/lib/Drupal/Core/Theme/Registry.php
@@ -29,6 +29,17 @@ use Symfony\Component\HttpKernel\HttpKernelInterface;
class Registry implements DestructableInterface {
/**
+ * A common key for storing preprocess invoke locations for modules.
+ *
+ * This is used to create a lookup for module preprocess implementations for
+ * being invoked in ThemeManager. The keys are a string representing the
+ * module and preprocess hook. The value is an array with the keys module
+ * and preprocess. This supports invoking preprocess hooks
+ * implemented using #[Preprocess] attributes or procedural functions.
+ */
+ private const string PREPROCESS_INVOKES = 'preprocess invokes';
+
+ /**
* The theme object representing the active theme for this registry.
*
* @var \Drupal\Core\Theme\ActiveTheme
@@ -162,6 +173,16 @@ class Registry implements DestructableInterface {
protected $moduleList;
/**
+ * Preprocess suggestions discovered in modules.
+ *
+ * These are stored to prevent adding preprocess suggestions to the invoke map
+ * that are not discovered in modules.
+ *
+ * @var array<string, true>
+ */
+ protected array $preprocessForSuggestions;
+
+ /**
* Constructs a \Drupal\Core\Theme\Registry object.
*
* @param string $root
@@ -184,8 +205,10 @@ class Registry implements DestructableInterface {
* The kernel.
* @param string $theme_name
* (optional) The name of the theme for which to construct the registry.
+ * @param array<string, true> $preprocess_for_suggestions
+ * (optional) Grouped preprocess functions from modules.
*/
- public function __construct($root, CacheBackendInterface $cache, LockBackendInterface $lock, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, ThemeInitializationInterface $theme_initialization, CacheBackendInterface $runtime_cache, ModuleExtensionList $module_list, protected HttpKernelInterface $kernel, $theme_name = NULL) {
+ public function __construct($root, CacheBackendInterface $cache, LockBackendInterface $lock, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, ThemeInitializationInterface $theme_initialization, CacheBackendInterface $runtime_cache, ModuleExtensionList $module_list, protected HttpKernelInterface $kernel, $theme_name = NULL, array $preprocess_for_suggestions = []) {
$this->root = $root;
$this->cache = $cache;
$this->lock = $lock;
@@ -195,6 +218,7 @@ class Registry implements DestructableInterface {
$this->runtimeCache = $runtime_cache;
$this->moduleList = $module_list;
$this->themeName = $theme_name;
+ $this->preprocessForSuggestions = $preprocess_for_suggestions;
}
/**
@@ -386,7 +410,10 @@ class Registry implements DestructableInterface {
* @see hook_theme_registry_alter()
*/
protected function build() {
- $cache = [];
+ $cache = [
+ static::PREPROCESS_INVOKES => [],
+ ];
+ $fixed_preprocess_functions = $this->collectModulePreprocess($cache, 'preprocess');
// First, preprocess the theme hooks advertised by modules. This will
// serve as the basic registry. Since the list of enabled modules is the
// same regardless of the theme used, this is cached in its own entry to
@@ -404,6 +431,7 @@ class Registry implements DestructableInterface {
$this->processExtension($cache, $module, 'module', $module, $this->moduleList->getPath($module));
});
}
+ $this->addFixedPreprocessFunctions($cache, $fixed_preprocess_functions);
// Only cache this registry if all modules are loaded.
if ($this->moduleHandler->isLoaded()) {
@@ -411,6 +439,8 @@ class Registry implements DestructableInterface {
}
}
+ $old_cache = $cache;
+
// Process each base theme.
// Ensure that we start with the root of the parents, so that both CSS files
// and preprocess functions comes first.
@@ -431,6 +461,10 @@ class Registry implements DestructableInterface {
// Hooks provided by the theme itself.
$this->processExtension($cache, $this->theme->getName(), 'theme', $this->theme->getName(), $this->theme->getPath());
+ // Add the fixed preprocess functions to hooks defined by themes. They
+ // were already added to hooks defined by modules and potentially cached.
+ $this->addFixedPreprocessFunctions($cache, $fixed_preprocess_functions, $old_cache);
+
// Discover and add all preprocess functions for theme hook suggestions.
$this->postProcessExtension($cache, $this->theme);
@@ -504,12 +538,9 @@ class Registry implements DestructableInterface {
'base hook' => TRUE,
];
- $module_list = array_keys($this->moduleHandler->getModuleList());
-
// Invoke the hook_theme() implementation, preprocess what is returned, and
// merge it into $cache.
$args = [$cache, $type, $theme, $path];
- $result = [];
if ($type === 'module') {
$result = $this->moduleHandler->invoke($name, 'theme', $args);
}
@@ -597,10 +628,7 @@ class Registry implements DestructableInterface {
// @todo trigger deprecation in https://www.drupal.org/project/drupal/issues/3513595.
$info['preprocess functions'][] = 'template_preprocess_' . $hook;
}
- // Add all modules so they can intervene with their own variable
- // preprocessors. This allows them to provide variable preprocessors
- // even if they are not the owner of the current hook.
- $prefixes = array_merge($prefixes, $module_list);
+ $info['preprocess functions'] = array_merge($info['preprocess functions'], $this->collectModulePreprocess($cache, 'preprocess_' . $hook));
}
elseif ($type == 'theme_engine' || $type == 'base_theme_engine') {
// Theme engines get an extra set that come before the normally
@@ -616,10 +644,7 @@ class Registry implements DestructableInterface {
$prefixes[] = $name;
}
foreach ($prefixes as $prefix) {
- // Only use non-hook-specific variable preprocessors for theming
- // hooks implemented as templates. See the @defgroup themeable
- // topic.
- if (isset($info['template']) && function_exists($prefix . '_preprocess')) {
+ if (function_exists($prefix . '_preprocess')) {
$info['preprocess functions'][] = $prefix . '_preprocess';
}
if (function_exists($prefix . '_preprocess_' . $hook)) {
@@ -649,14 +674,15 @@ class Registry implements DestructableInterface {
// template.
if ($type == 'theme' || $type == 'base_theme') {
foreach ($cache as $hook => $info) {
+ if ($hook == static::PREPROCESS_INVOKES) {
+ continue;
+ }
// Check only if not registered by the theme or engine.
if (empty($result[$hook])) {
if (!isset($info['preprocess functions'])) {
$cache[$hook]['preprocess functions'] = [];
}
- // Only use non-hook-specific variable preprocessors for theme hooks
- // implemented as templates. See the @defgroup themeable topic.
- if (isset($info['template']) && function_exists($name . '_preprocess')) {
+ if (function_exists($name . '_preprocess')) {
$cache[$hook]['preprocess functions'][] = $name . '_preprocess';
}
if (function_exists($name . '_preprocess_' . $hook)) {
@@ -761,7 +787,7 @@ class Registry implements DestructableInterface {
// Collect all variable preprocess functions in the correct order.
$suggestion_level = [];
- $matches = [];
+ $invokes = [];
// Look for functions named according to the pattern and add them if they
// have matching hooks in the registry.
foreach ($prefixes as $prefix) {
@@ -778,6 +804,10 @@ class Registry implements DestructableInterface {
if (isset($cache[$matches[2]])) {
$level = substr_count($matches[1], '__');
$suggestion_level[$level][$candidate] = $matches[1];
+ $module_preprocess_function = $prefix . '_preprocess_' . $matches[1];
+ if (isset($this->preprocessForSuggestions[$module_preprocess_function])) {
+ $invokes[$candidate] = ['module' => $prefix, 'hook' => 'preprocess_' . $matches[1]];
+ }
}
}
}
@@ -795,6 +825,9 @@ class Registry implements DestructableInterface {
if (isset($cache[$hook]['preprocess functions']) && !in_array($preprocessor, $cache[$hook]['preprocess functions'])) {
// Add missing preprocessor to existing hook.
$cache[$hook]['preprocess functions'][] = $preprocessor;
+ if (isset($invokes[$preprocessor])) {
+ $cache[static::PREPROCESS_INVOKES][$preprocessor] = $invokes[$preprocessor];
+ }
}
elseif (!isset($cache[$hook]) && strpos($hook, '__')) {
// Process non-existing hook and register it.
@@ -802,6 +835,9 @@ class Registry implements DestructableInterface {
// suggestion hook or the base hook.
$this->completeSuggestion($hook, $cache);
$cache[$hook]['preprocess functions'][] = $preprocessor;
+ if (isset($invokes[$preprocessor])) {
+ $cache[static::PREPROCESS_INVOKES][$preprocessor] = $invokes[$preprocessor];
+ }
}
}
}
@@ -809,6 +845,9 @@ class Registry implements DestructableInterface {
// hooks. This ensures that derivative hooks have a complete set of variable
// preprocess functions.
foreach ($cache as $hook => $info) {
+ if ($hook == static::PREPROCESS_INVOKES) {
+ continue;
+ }
// The 'base hook' is only applied to derivative hooks already registered
// from a pattern. This is typically set from
// drupal_find_theme_templates().
@@ -890,6 +929,7 @@ class Registry implements DestructableInterface {
$theme_functions = $functions['user'];
}
+ $theme_functions = array_merge($theme_functions, array_keys($this->preprocessForSuggestions));
$grouped_functions = [];
// Splitting user defined functions into groups by the first prefix.
foreach ($theme_functions as $function) {
@@ -900,4 +940,60 @@ class Registry implements DestructableInterface {
return $grouped_functions;
}
+ /**
+ * Adds $prefix_preprocess functions to every hook.
+ *
+ * @param array $cache
+ * The theme registry, as documented in
+ * \Drupal\Core\Theme\Registry::processExtension().
+ * @param array $fixed_preprocess_functions
+ * A list of preprocess functions.
+ * @param array $old_cache
+ * An already processed theme registry.
+ */
+ protected function addFixedPreprocessFunctions(array &$cache, array $fixed_preprocess_functions, array $old_cache = []): void {
+ foreach (array_keys(array_diff_key($cache, $old_cache)) as $hook) {
+ if ($hook == static::PREPROCESS_INVOKES) {
+ continue;
+ }
+ if (!isset($cache[$hook]['preprocess functions'])) {
+ $cache[$hook]['preprocess functions'] = $fixed_preprocess_functions;
+ }
+ else {
+ $offset = 0;
+ while (isset($cache[$hook]['preprocess functions'][$offset]) && is_string($cache[$hook]['preprocess functions'][$offset]) && str_starts_with($cache[$hook]['preprocess functions'][$offset], 'template_')) {
+ $offset++;
+ }
+ array_splice($cache[$hook]['preprocess functions'], $offset, 0, $fixed_preprocess_functions);
+ }
+ }
+ }
+
+ /**
+ * Collect module implementations of a single hook.
+ *
+ * @param array $cache
+ * The preprocess hook.
+ * @param string $hook
+ * The theme registry, as documented in
+ * \Drupal\Core\Theme\Registry::processExtension().
+ *
+ * @return array
+ * A list of preprocess functions.
+ */
+ protected function collectModulePreprocess(array &$cache, string $hook): array {
+ $preprocess_functions = [];
+ // This is used so we can collect all preprocess functions in modules but
+ // prevent them from being executed. Registry needs to cache preprocess
+ // functions so we only want to gather the ones that exist, but we do not
+ // want to execute them. Callable is not used so that preprocess
+ // implementations are not executed.
+ $this->moduleHandler->invokeAllWith($hook, function (callable $callable, string $module) use ($hook, &$cache, &$preprocess_functions) {
+ $function = $module . '_' . $hook;
+ $cache[static::PREPROCESS_INVOKES][$function] = ['module' => $module, 'hook' => $hook];
+ $preprocess_functions[] = $function;
+ });
+ return $preprocess_functions;
+ }
+
}
diff --git a/core/lib/Drupal/Core/Theme/ThemeManager.php b/core/lib/Drupal/Core/Theme/ThemeManager.php
index 8962896ee3ef..3724ea6e1563 100644
--- a/core/lib/Drupal/Core/Theme/ThemeManager.php
+++ b/core/lib/Drupal/Core/Theme/ThemeManager.php
@@ -187,6 +187,7 @@ class ThemeManager implements ThemeManagerInterface {
}
$info = $theme_registry->get($hook);
+ $invoke_map = $theme_registry->getPreprocessInvokes();
if (isset($info['deprecated'])) {
@trigger_error($info['deprecated'], E_USER_DEPRECATED);
}
@@ -293,7 +294,17 @@ class ThemeManager implements ThemeManagerInterface {
// overridden. See \Drupal\Core\Theme\Registry.
if (isset($info['preprocess functions'])) {
foreach ($info['preprocess functions'] as $preprocessor_function) {
- if (is_callable($preprocessor_function)) {
+ // Preprocess hooks are stored as strings resembling functions.
+ // This is for backwards compatibility and may represent OOP
+ // implementations as well.
+ if (is_string($preprocessor_function) && isset($invoke_map[$preprocessor_function])) {
+ // While themes are not modules, ModuleHandlerInterface::invoke calls
+ // a legacy invoke which can can call any extension, not just
+ // modules.
+ $this->moduleHandler->invoke(... $invoke_map[$preprocessor_function], args: [&$variables, $hook, $info]);
+ }
+ // Check if hook_theme_registry_alter added a manual callback.
+ elseif (is_callable($preprocessor_function)) {
call_user_func_array($preprocessor_function, [&$variables, $hook, $info]);
}
}
diff --git a/core/lib/Drupal/Core/Utility/ThemeRegistry.php b/core/lib/Drupal/Core/Utility/ThemeRegistry.php
index d0ca9918dd23..a6846ba263c3 100644
--- a/core/lib/Drupal/Core/Utility/ThemeRegistry.php
+++ b/core/lib/Drupal/Core/Utility/ThemeRegistry.php
@@ -168,4 +168,16 @@ class ThemeRegistry extends CacheCollector implements DestructableInterface {
}
}
+ /**
+ * Gets preprocess invoke map.
+ *
+ * @return array
+ * An array of preprocess invokes for preprocess functions in modules.
+ *
+ * @internal
+ */
+ public function getPreprocessInvokes() {
+ return $this->get('preprocess invokes');
+ }
+
}
diff --git a/core/modules/announcements_feed/announcements_feed.services.yml b/core/modules/announcements_feed/announcements_feed.services.yml
index b9e88231be16..ea927c887d1f 100644
--- a/core/modules/announcements_feed/announcements_feed.services.yml
+++ b/core/modules/announcements_feed/announcements_feed.services.yml
@@ -1,7 +1,7 @@
parameters:
announcements_feed.feed_json_url: https://www.drupal.org/announcements.json
announcements_feed.feed_link: https://www.drupal.org/about/announcements
- announcements_feed.hooks_converted: true
+ announcements_feed.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/automated_cron/automated_cron.services.yml b/core/modules/automated_cron/automated_cron.services.yml
index bbb55f5f6f39..0cc66d22384e 100644
--- a/core/modules/automated_cron/automated_cron.services.yml
+++ b/core/modules/automated_cron/automated_cron.services.yml
@@ -1,5 +1,5 @@
parameters:
- automated_cron.hooks_converted: true
+ automated_cron.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/ban/ban.services.yml b/core/modules/ban/ban.services.yml
index 7bd8a245e5f4..585f8fa0d60a 100644
--- a/core/modules/ban/ban.services.yml
+++ b/core/modules/ban/ban.services.yml
@@ -1,5 +1,5 @@
parameters:
- ban.hooks_converted: true
+ ban.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/basic_auth/basic_auth.services.yml b/core/modules/basic_auth/basic_auth.services.yml
index 09b21c68f33d..af0de75629e9 100644
--- a/core/modules/basic_auth/basic_auth.services.yml
+++ b/core/modules/basic_auth/basic_auth.services.yml
@@ -1,5 +1,5 @@
parameters:
- basic_auth.hooks_converted: true
+ basic_auth.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/block_content/block_content.services.yml b/core/modules/block_content/block_content.services.yml
index eb8b9924ce9f..2661eda513b5 100644
--- a/core/modules/block_content/block_content.services.yml
+++ b/core/modules/block_content/block_content.services.yml
@@ -1,5 +1,5 @@
parameters:
- block_content.hooks_converted: true
+ block_content.skip_procedural_hook_scan: false
services:
_defaults:
diff --git a/core/modules/breakpoint/breakpoint.services.yml b/core/modules/breakpoint/breakpoint.services.yml
index 7558eb6d7650..46d8f0d27e16 100644
--- a/core/modules/breakpoint/breakpoint.services.yml
+++ b/core/modules/breakpoint/breakpoint.services.yml
@@ -1,5 +1,5 @@
parameters:
- breakpoint.hooks_converted: true
+ breakpoint.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index b357f3069aeb..139513c1c37c 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -111,15 +111,6 @@ function comment_preview(CommentInterface $comment, FormStateInterface $form_sta
}
/**
- * Implements hook_preprocess_HOOK() for block templates.
- */
-function comment_preprocess_block(&$variables): void {
- if ($variables['configuration']['provider'] == 'comment') {
- $variables['attributes']['role'] = 'navigation';
- }
-}
-
-/**
* Prepares variables for comment templates.
*
* By default this function performs special preprocessing of some base fields
diff --git a/core/modules/comment/comment.services.yml b/core/modules/comment/comment.services.yml
index 22cc8f9a428b..7f9e82245518 100644
--- a/core/modules/comment/comment.services.yml
+++ b/core/modules/comment/comment.services.yml
@@ -1,5 +1,5 @@
parameters:
- comment.hooks_converted: true
+ comment.skip_procedural_hook_scan: false
services:
_defaults:
diff --git a/core/modules/comment/src/Hook/CommentThemeHooks.php b/core/modules/comment/src/Hook/CommentThemeHooks.php
new file mode 100644
index 000000000000..e789af6dab10
--- /dev/null
+++ b/core/modules/comment/src/Hook/CommentThemeHooks.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace Drupal\comment\Hook;
+
+use Drupal\Core\Hook\Attribute\Preprocess;
+
+/**
+ * Hook implementations for comment.
+ */
+class CommentThemeHooks {
+
+ /**
+ * Implements hook_preprocess_HOOK() for block templates.
+ */
+ #[Preprocess('block')]
+ public function preprocessBlock(&$variables): void {
+ if ($variables['configuration']['provider'] == 'comment') {
+ $variables['attributes']['role'] = 'navigation';
+ }
+ }
+
+}
diff --git a/core/modules/comment/tests/modules/comment_empty_title_test/comment_empty_title_test.module b/core/modules/comment/tests/modules/comment_empty_title_test/comment_empty_title_test.module
deleted file mode 100644
index c107fd11466f..000000000000
--- a/core/modules/comment/tests/modules/comment_empty_title_test/comment_empty_title_test.module
+++ /dev/null
@@ -1,15 +0,0 @@
-<?php
-
-/**
- * @file
- * Empties comment titles to test markup in edge case scenarios.
- */
-
-declare(strict_types=1);
-
-/**
- * Implements hook_preprocess_comment().
- */
-function comment_empty_title_test_preprocess_comment(&$variables): void {
- $variables['title'] = '';
-}
diff --git a/core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php b/core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php
new file mode 100644
index 000000000000..db1ffae5a6d0
--- /dev/null
+++ b/core/modules/comment/tests/modules/comment_empty_title_test/src/Hook/CommentEmptyTitleTestThemeHooks.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\comment_empty_title_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Preprocess;
+
+/**
+ * Hook implementations for comment_empty_title_test.
+ */
+class CommentEmptyTitleTestThemeHooks {
+
+ /**
+ * Implements hook_preprocess_comment().
+ */
+ #[Preprocess('comment')]
+ public function preprocessComment(&$variables): void {
+ $variables['title'] = '';
+ }
+
+}
diff --git a/core/modules/config/config.services.yml b/core/modules/config/config.services.yml
index 8667240a82f2..49a17dd40081 100644
--- a/core/modules/config/config.services.yml
+++ b/core/modules/config/config.services.yml
@@ -1,5 +1,5 @@
parameters:
- config.hooks_converted: true
+ config.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/config_translation/config_translation.services.yml b/core/modules/config_translation/config_translation.services.yml
index 5c35e5ee588b..458905d06292 100644
--- a/core/modules/config_translation/config_translation.services.yml
+++ b/core/modules/config_translation/config_translation.services.yml
@@ -1,5 +1,5 @@
parameters:
- config_translation.hooks_converted: true
+ config_translation.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/contact/contact.services.yml b/core/modules/contact/contact.services.yml
index b078fd2cfdc4..61ec8dd7fdac 100644
--- a/core/modules/contact/contact.services.yml
+++ b/core/modules/contact/contact.services.yml
@@ -1,5 +1,5 @@
parameters:
- contact.hooks_converted: true
+ contact.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/content_moderation/content_moderation.services.yml b/core/modules/content_moderation/content_moderation.services.yml
index 85abe7b2f7cd..8977847173c4 100644
--- a/core/modules/content_moderation/content_moderation.services.yml
+++ b/core/modules/content_moderation/content_moderation.services.yml
@@ -1,5 +1,5 @@
parameters:
- content_moderation.hooks_converted: true
+ content_moderation.skip_procedural_hook_scan: false
services:
_defaults:
diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module
index f6c3335b619b..8daec06c8ee5 100644
--- a/core/modules/contextual/contextual.module
+++ b/core/modules/contextual/contextual.module
@@ -8,46 +8,6 @@ use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Language\LanguageInterface;
/**
- * Implements hook_preprocess().
- *
- * @see \Drupal\contextual\Element\ContextualLinksPlaceholder
- * @see contextual_page_attachments()
- * @see \Drupal\contextual\ContextualController::render()
- */
-function contextual_preprocess(&$variables, $hook, $info): void {
- // Determine the primary theme function argument.
- if (!empty($info['variables'])) {
- $keys = array_keys($info['variables']);
- $key = $keys[0];
- }
- elseif (!empty($info['render element'])) {
- $key = $info['render element'];
- }
- if (!empty($key) && isset($variables[$key])) {
- $element = $variables[$key];
- }
-
- if (isset($element) && is_array($element) && !empty($element['#contextual_links'])) {
- $variables['#cache']['contexts'][] = 'user.permissions';
- if (\Drupal::currentUser()->hasPermission('access contextual links')) {
- // Mark this element as potentially having contextual links attached to
- // it.
- $variables['attributes']['class'][] = 'contextual-region';
-
- // Renders a contextual links placeholder unconditionally, thus not
- // breaking the render cache. Although the empty placeholder is rendered
- // for all users, contextual_page_attachments() only adds the asset
- // library for users with the 'access contextual links' permission, thus
- // preventing unnecessary HTTP requests for users without that permission.
- $variables['title_suffix']['contextual_links'] = [
- '#type' => 'contextual_links_placeholder',
- '#id' => _contextual_links_to_id($element['#contextual_links']),
- ];
- }
- }
-}
-
-/**
* Serializes #contextual_links property value array to a string.
*
* Examples:
diff --git a/core/modules/contextual/contextual.services.yml b/core/modules/contextual/contextual.services.yml
index dff437ec3393..558e7d029ae5 100644
--- a/core/modules/contextual/contextual.services.yml
+++ b/core/modules/contextual/contextual.services.yml
@@ -1,2 +1,2 @@
parameters:
- contextual.hooks_converted: true
+ contextual.skip_procedural_hook_scan: true
diff --git a/core/modules/contextual/src/Hook/ContextualThemeHooks.php b/core/modules/contextual/src/Hook/ContextualThemeHooks.php
new file mode 100644
index 000000000000..47db1f9bde6a
--- /dev/null
+++ b/core/modules/contextual/src/Hook/ContextualThemeHooks.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\contextual\Hook;
+
+use Drupal\Core\Hook\Attribute\Preprocess;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Hook implementations for contextual.
+ */
+class ContextualThemeHooks {
+
+ public function __construct(
+ protected readonly AccountInterface $currentUser,
+ ) {}
+
+ /**
+ * Implements hook_preprocess().
+ *
+ * @see \Drupal\contextual\Element\ContextualLinksPlaceholder
+ * @see contextual_page_attachments()
+ * @see \Drupal\contextual\ContextualController::render()
+ */
+ #[Preprocess]
+ public function preprocess(&$variables, $hook, $info): void {
+ // Determine the primary theme function argument.
+ if (!empty($info['variables'])) {
+ $keys = array_keys($info['variables']);
+ $key = $keys[0];
+ }
+ elseif (!empty($info['render element'])) {
+ $key = $info['render element'];
+ }
+ if (!empty($key) && isset($variables[$key])) {
+ $element = $variables[$key];
+ }
+
+ if (isset($element) && is_array($element) && !empty($element['#contextual_links'])) {
+ $variables['#cache']['contexts'][] = 'user.permissions';
+ if ($this->currentUser->hasPermission('access contextual links')) {
+ // Mark this element as potentially having contextual links attached to it.
+ $variables['attributes']['class'][] = 'contextual-region';
+
+ // Renders a contextual links placeholder unconditionally, thus not breaking
+ // the render cache. Although the empty placeholder is rendered for all
+ // users, contextual_page_attachments() only adds the asset library for
+ // users with the 'access contextual links' permission, thus preventing
+ // unnecessary HTTP requests for users without that permission.
+ $variables['title_suffix']['contextual_links'] = [
+ '#type' => 'contextual_links_placeholder',
+ '#id' => _contextual_links_to_id($element['#contextual_links']),
+ ];
+ }
+ }
+ }
+
+}
diff --git a/core/modules/datetime/datetime.services.yml b/core/modules/datetime/datetime.services.yml
index af7d5c9a4d11..3f9c511d1e74 100644
--- a/core/modules/datetime/datetime.services.yml
+++ b/core/modules/datetime/datetime.services.yml
@@ -1,5 +1,5 @@
parameters:
- datetime.hooks_converted: true
+ datetime.skip_procedural_hook_scan: true
services:
datetime.views_helper:
diff --git a/core/modules/datetime_range/datetime_range.services.yml b/core/modules/datetime_range/datetime_range.services.yml
index 6234cb0e6872..6701b44dd713 100644
--- a/core/modules/datetime_range/datetime_range.services.yml
+++ b/core/modules/datetime_range/datetime_range.services.yml
@@ -1,2 +1,2 @@
parameters:
- datetime_range.hooks_converted: true
+ datetime_range.skip_procedural_hook_scan: true
diff --git a/core/modules/dblog/dblog.services.yml b/core/modules/dblog/dblog.services.yml
index 50398593fe98..477f913e6bfd 100644
--- a/core/modules/dblog/dblog.services.yml
+++ b/core/modules/dblog/dblog.services.yml
@@ -1,5 +1,5 @@
parameters:
- dblog.hooks_converted: true
+ dblog.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/dynamic_page_cache/dynamic_page_cache.services.yml b/core/modules/dynamic_page_cache/dynamic_page_cache.services.yml
index 0ec8b7c2f8da..8cee8fd0d8b9 100644
--- a/core/modules/dynamic_page_cache/dynamic_page_cache.services.yml
+++ b/core/modules/dynamic_page_cache/dynamic_page_cache.services.yml
@@ -1,5 +1,5 @@
parameters:
- dynamic_page_cache.hooks_converted: true
+ dynamic_page_cache.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/editor/editor.services.yml b/core/modules/editor/editor.services.yml
index 0db1dc61968c..46f674b27ab9 100644
--- a/core/modules/editor/editor.services.yml
+++ b/core/modules/editor/editor.services.yml
@@ -1,5 +1,5 @@
parameters:
- editor.hooks_converted: true
+ editor.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/field/field.services.yml b/core/modules/field/field.services.yml
index 8b0f5a33093f..3e986982cdbb 100644
--- a/core/modules/field/field.services.yml
+++ b/core/modules/field/field.services.yml
@@ -1,5 +1,5 @@
parameters:
- field.hooks_converted: true
+ field.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/field_layout/field_layout.services.yml b/core/modules/field_layout/field_layout.services.yml
index e099e35a9533..48e3a6abe6f9 100644
--- a/core/modules/field_layout/field_layout.services.yml
+++ b/core/modules/field_layout/field_layout.services.yml
@@ -1,2 +1,2 @@
parameters:
- field_layout.hooks_converted: true
+ field_layout.skip_procedural_hook_scan: true
diff --git a/core/modules/field_ui/field_ui.services.yml b/core/modules/field_ui/field_ui.services.yml
index dd094ee72aae..194b7287cef2 100644
--- a/core/modules/field_ui/field_ui.services.yml
+++ b/core/modules/field_ui/field_ui.services.yml
@@ -1,5 +1,5 @@
parameters:
- field_ui.hooks_converted: true
+ field_ui.skip_procedural_hook_scan: false
services:
_defaults:
diff --git a/core/modules/filter/filter.services.yml b/core/modules/filter/filter.services.yml
index 6b9c243c7ab9..58caf58f76c0 100644
--- a/core/modules/filter/filter.services.yml
+++ b/core/modules/filter/filter.services.yml
@@ -1,5 +1,5 @@
parameters:
- filter.hooks_converted: true
+ filter.skip_procedural_hook_scan: false
services:
_defaults:
diff --git a/core/modules/help/help.services.yml b/core/modules/help/help.services.yml
index 21a8e2f3adf1..2cb3ecbd8ec5 100644
--- a/core/modules/help/help.services.yml
+++ b/core/modules/help/help.services.yml
@@ -1,5 +1,5 @@
parameters:
- help.hooks_converted: true
+ help.skip_procedural_hook_scan: false
services:
_defaults:
diff --git a/core/modules/history/history.services.yml b/core/modules/history/history.services.yml
index 7b2b654310f2..fc83944febaf 100644
--- a/core/modules/history/history.services.yml
+++ b/core/modules/history/history.services.yml
@@ -1,2 +1,2 @@
parameters:
- history.hooks_converted: true
+ history.skip_procedural_hook_scan: true
diff --git a/core/modules/inline_form_errors/inline_form_errors.services.yml b/core/modules/inline_form_errors/inline_form_errors.services.yml
index 0358ee160271..f3ae13843ace 100644
--- a/core/modules/inline_form_errors/inline_form_errors.services.yml
+++ b/core/modules/inline_form_errors/inline_form_errors.services.yml
@@ -1,2 +1,2 @@
parameters:
- inline_form_errors.hooks_converted: true
+ inline_form_errors.skip_procedural_hook_scan: false
diff --git a/core/modules/language/language.services.yml b/core/modules/language/language.services.yml
index 2fc8f9ada778..93f994c4f67e 100644
--- a/core/modules/language/language.services.yml
+++ b/core/modules/language/language.services.yml
@@ -1,5 +1,5 @@
parameters:
- language.hooks_converted: true
+ language.skip_procedural_hook_scan: false
services:
_defaults:
diff --git a/core/modules/link/link.services.yml b/core/modules/link/link.services.yml
index becc0b945dd7..de8f823e8cb9 100644
--- a/core/modules/link/link.services.yml
+++ b/core/modules/link/link.services.yml
@@ -1,2 +1,2 @@
parameters:
- link.hooks_converted: true
+ link.skip_procedural_hook_scan: false
diff --git a/core/modules/locale/locale.batch.inc b/core/modules/locale/locale.batch.inc
index 780333876415..0f204b6af2df 100644
--- a/core/modules/locale/locale.batch.inc
+++ b/core/modules/locale/locale.batch.inc
@@ -7,7 +7,7 @@
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\Exception\InvalidStreamWrapperException;
use Drupal\Core\File\FileExists;
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\Url;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
@@ -37,7 +37,7 @@ require_once __DIR__ . '/locale.translation.inc';
* @param array|\ArrayAccess $context
* The batch context.
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function locale_translation_batch_version_check(string $project, string $langcode, array|\ArrayAccess &$context): void {
$locale_project = \Drupal::service('locale.project')->get($project);
diff --git a/core/modules/locale/locale.bulk.inc b/core/modules/locale/locale.bulk.inc
index e8396622d616..0cb5e9058566 100644
--- a/core/modules/locale/locale.bulk.inc
+++ b/core/modules/locale/locale.bulk.inc
@@ -6,7 +6,7 @@
use Drupal\Core\Batch\BatchBuilder;
use Drupal\Core\File\Exception\FileException;
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Url;
use Drupal\file\FileInterface;
@@ -37,7 +37,7 @@ use Drupal\locale\Gettext;
* l10n_update functionality to feed in translation files alike.
* https://www.drupal.org/node/1191488
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function locale_translate_batch_import_files(array $options, $force = FALSE) {
$options += [
'overwrite_options' => [],
diff --git a/core/modules/locale/locale.compare.inc b/core/modules/locale/locale.compare.inc
index a1ddbf61a4f8..069f200942f5 100644
--- a/core/modules/locale/locale.compare.inc
+++ b/core/modules/locale/locale.compare.inc
@@ -5,7 +5,7 @@
*/
use Drupal\Core\Batch\BatchBuilder;
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\Utility\ProjectInfo;
/**
@@ -18,7 +18,7 @@ require_once __DIR__ . '/locale.translation.inc';
/**
* Clear the project data table.
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function locale_translation_flush_projects(): void {
\Drupal::service('locale.project')->deleteAll();
}
diff --git a/core/modules/locale/locale.fetch.inc b/core/modules/locale/locale.fetch.inc
index 34d0fda2e94a..678e1e25e58a 100644
--- a/core/modules/locale/locale.fetch.inc
+++ b/core/modules/locale/locale.fetch.inc
@@ -5,7 +5,7 @@
*/
use Drupal\Core\Batch\BatchBuilder;
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
/**
* Load the common translation API.
@@ -28,7 +28,7 @@ require_once __DIR__ . '/locale.translation.inc';
* @return array
* Batch definition array.
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function locale_translation_batch_update_build($projects = [], $langcodes = [], $options = []) {
\Drupal::moduleHandler()->loadInclude('locale', 'inc', 'locale.compare');
$projects = $projects ?: array_keys(locale_translation_get_projects());
diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install
index 30aa89000b8a..f583df4426eb 100644
--- a/core/modules/locale/locale.install
+++ b/core/modules/locale/locale.install
@@ -7,7 +7,7 @@
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileSystemInterface;
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\Link;
use Drupal\Core\Url;
@@ -78,7 +78,7 @@ function locale_requirements($phase): array {
/**
* Implements hook_install().
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function locale_install(): void {
// Create the interface translations directory and ensure it's writable.
if (!$directory = \Drupal::config('locale.settings')->get('translation.path')) {
diff --git a/core/modules/locale/locale.module b/core/modules/locale/locale.module
index ba7cbc15122e..3a6856d4c07f 100644
--- a/core/modules/locale/locale.module
+++ b/core/modules/locale/locale.module
@@ -11,11 +11,10 @@ use Drupal\Component\Utility\UrlHelper;
use Drupal\Component\Utility\Xss;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileSystemInterface;
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\Installer\InstallerKernel;
use Drupal\Core\Site\Settings;
use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Language\LanguageInterface;
use Drupal\Component\Utility\Crypt;
use Drupal\locale\LocaleEvent;
use Drupal\locale\LocaleEvents;
@@ -146,7 +145,7 @@ const LOCALE_TRANSLATION_CURRENT = 'current';
* Array of installed languages keyed by language name. English is omitted
* unless it is marked as translatable.
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function locale_translatable_language_list() {
$languages = \Drupal::languageManager()->getLanguages();
if (!locale_is_translatable('en')) {
@@ -443,30 +442,6 @@ function locale_system_file_system_settings_submit(&$form, FormStateInterface $f
}
/**
- * Implements hook_preprocess_HOOK() for node templates.
- */
-function locale_preprocess_node(&$variables): void {
- /** @var \Drupal\node\NodeInterface $node */
- $node = $variables['node'];
- if ($node->language()->getId() != LanguageInterface::LANGCODE_NOT_SPECIFIED) {
- $interface_language = \Drupal::languageManager()->getCurrentLanguage();
-
- $node_language = $node->language();
- if ($node_language->getId() != $interface_language->getId()) {
- // If the node language was different from the page language, we should
- // add markup to identify the language. Otherwise the page language is
- // inherited.
- $variables['attributes']['lang'] = $node_language->getId();
- if ($node_language->getDirection() != $interface_language->getDirection()) {
- // If text direction is different form the page's text direction, add
- // direction information as well.
- $variables['attributes']['dir'] = $node_language->getDirection();
- }
- }
- }
-}
-
-/**
* Gets current translation status from the {locale_file} table.
*
* @return array
diff --git a/core/modules/locale/locale.pages.inc b/core/modules/locale/locale.pages.inc
index e016a1a99a3f..aef8c5f6fe5f 100644
--- a/core/modules/locale/locale.pages.inc
+++ b/core/modules/locale/locale.pages.inc
@@ -4,7 +4,7 @@
* @file
*/
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\Link;
use Drupal\Core\Url;
@@ -22,7 +22,7 @@ use Drupal\Core\Url;
*
* @see \Drupal\locale\Form\TranslationStatusForm
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function template_preprocess_locale_translation_update_info(array &$variables): void {
foreach ($variables['updates'] as $update) {
$variables['modules'][] = $update['name'];
diff --git a/core/modules/locale/locale.translation.inc b/core/modules/locale/locale.translation.inc
index 7f48a5d3fa0f..c82ba2e86663 100644
--- a/core/modules/locale/locale.translation.inc
+++ b/core/modules/locale/locale.translation.inc
@@ -4,7 +4,7 @@
* @file
*/
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\StreamWrapper\StreamWrapperManager;
/**
@@ -55,7 +55,7 @@ const LOCALE_TRANSLATION_SOURCE_COMPARE_GT = 1;
*
* @see locale_translation_build_projects()
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function locale_translation_get_projects(array $project_names = []) {
$projects = &drupal_static(__FUNCTION__, []);
diff --git a/core/modules/locale/src/Hook/LocaleThemeHooks.php b/core/modules/locale/src/Hook/LocaleThemeHooks.php
new file mode 100644
index 000000000000..d1e438f50ace
--- /dev/null
+++ b/core/modules/locale/src/Hook/LocaleThemeHooks.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\locale\Hook;
+
+use Drupal\Core\Hook\Attribute\Preprocess;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+
+/**
+ * Hook implementations for locale.
+ */
+class LocaleThemeHooks {
+
+ public function __construct(
+ protected readonly LanguageManagerInterface $languageManager,
+ ) {}
+
+ /**
+ * Implements hook_preprocess_HOOK() for node templates.
+ */
+ #[Preprocess('node')]
+ public function preprocessNode(&$variables): void {
+ /** @var \Drupal\node\NodeInterface $node */
+ $node = $variables['node'];
+ if ($node->language()->getId() != LanguageInterface::LANGCODE_NOT_SPECIFIED) {
+ $interface_language = $this->languageManager->getCurrentLanguage();
+
+ $node_language = $node->language();
+ if ($node_language->getId() != $interface_language->getId()) {
+ // If the node language was different from the page language, we should
+ // add markup to identify the language. Otherwise the page language is
+ // inherited.
+ $variables['attributes']['lang'] = $node_language->getId();
+ if ($node_language->getDirection() != $interface_language->getDirection()) {
+ // If text direction is different form the page's text direction, add
+ // direction information as well.
+ $variables['attributes']['dir'] = $node_language->getDirection();
+ }
+ }
+ }
+ }
+
+}
diff --git a/core/modules/media/media.install b/core/modules/media/media.install
index f0acbf482da4..48ed664fdef7 100644
--- a/core/modules/media/media.install
+++ b/core/modules/media/media.install
@@ -8,7 +8,7 @@
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileExists;
use Drupal\Core\File\FileSystemInterface;
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\image\Plugin\Field\FieldType\ImageItem;
@@ -112,7 +112,7 @@ function media_requirements($phase): array {
/**
* Implements hook_install().
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function media_install(): void {
$source = \Drupal::service('extension.list.module')->getPath('media') . '/images/icons';
$destination = \Drupal::config('media.settings')->get('icon_base_uri');
diff --git a/core/modules/media/media.module b/core/modules/media/media.module
index 537cbee18620..dbf031d16a9a 100644
--- a/core/modules/media/media.module
+++ b/core/modules/media/media.module
@@ -5,7 +5,7 @@
*/
use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\RenderElementBase;
use Drupal\Core\Template\Attribute;
@@ -23,7 +23,6 @@ use Drupal\Core\Url;
* - name: The label for the media item.
* - view_mode: View mode; e.g., 'full', 'teaser', etc.
*/
-#[StopProceduralHookScan]
function template_preprocess_media(array &$variables): void {
$variables['media'] = $variables['elements']['#media'];
$variables['view_mode'] = $variables['elements']['#view_mode'];
@@ -74,6 +73,7 @@ function media_preprocess_media_reference_help(&$variables): void {
* @internal
* This function is internal and may be removed in a minor release.
*/
+#[ProceduralHookScanStop]
function _media_get_add_url($allowed_bundles) {
$access_handler = \Drupal::entityTypeManager()->getAccessControlHandler('media');
$create_bundles = array_filter($allowed_bundles, [$access_handler, 'createAccess']);
diff --git a/core/modules/media_library/media_library.services.yml b/core/modules/media_library/media_library.services.yml
index 510769de9d6d..7c0d2131601a 100644
--- a/core/modules/media_library/media_library.services.yml
+++ b/core/modules/media_library/media_library.services.yml
@@ -1,5 +1,5 @@
parameters:
- media_library.hooks_converted: true
+ media_library.skip_procedural_hook_scan: false
services:
_defaults:
diff --git a/core/modules/menu_link_content/menu_link_content.services.yml b/core/modules/menu_link_content/menu_link_content.services.yml
index 63d3867b1adc..e1a01a283f95 100644
--- a/core/modules/menu_link_content/menu_link_content.services.yml
+++ b/core/modules/menu_link_content/menu_link_content.services.yml
@@ -1,2 +1,2 @@
parameters:
- menu_link_content.hooks_converted: true
+ menu_link_content.skip_procedural_hook_scan: true
diff --git a/core/modules/menu_ui/menu_ui.services.yml b/core/modules/menu_ui/menu_ui.services.yml
index 962a86b9c3df..45682dc6e4d1 100644
--- a/core/modules/menu_ui/menu_ui.services.yml
+++ b/core/modules/menu_ui/menu_ui.services.yml
@@ -1,5 +1,5 @@
parameters:
- menu_ui.hooks_converted: true
+ menu_ui.skip_procedural_hook_scan: false
services:
_defaults:
diff --git a/core/modules/migrate/migrate.services.yml b/core/modules/migrate/migrate.services.yml
index 2af458d08270..8b2e40285719 100644
--- a/core/modules/migrate/migrate.services.yml
+++ b/core/modules/migrate/migrate.services.yml
@@ -1,5 +1,5 @@
parameters:
- migrate.hooks_converted: true
+ migrate.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/migrate_drupal/migrate_drupal.services.yml b/core/modules/migrate_drupal/migrate_drupal.services.yml
index e0d16a99459d..358992f62dfb 100644
--- a/core/modules/migrate_drupal/migrate_drupal.services.yml
+++ b/core/modules/migrate_drupal/migrate_drupal.services.yml
@@ -1,5 +1,5 @@
parameters:
- migrate_drupal.hooks_converted: true
+ migrate_drupal.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/migrate_drupal_ui/migrate_drupal_ui.services.yml b/core/modules/migrate_drupal_ui/migrate_drupal_ui.services.yml
index 59ef9f3a1de6..d4cd82212c5d 100644
--- a/core/modules/migrate_drupal_ui/migrate_drupal_ui.services.yml
+++ b/core/modules/migrate_drupal_ui/migrate_drupal_ui.services.yml
@@ -1,5 +1,5 @@
parameters:
- migrate_drupal_ui.hooks_converted: true
+ migrate_drupal_ui.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index e2b0fcccb2e1..4ee3c48a00b2 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -257,30 +257,6 @@ function node_preprocess_block(&$variables): void {
}
/**
- * Implements hook_preprocess_HOOK() for node field templates.
- */
-function node_preprocess_field__node(&$variables): void {
- // Set a variable 'is_inline' in cases where inline markup is required,
- // without any block elements such as <div>.
-
- if ($variables['element']['#is_page_title'] ?? FALSE) {
- // Page title is always inline because it will be displayed inside <h1>.
- $variables['is_inline'] = TRUE;
- }
- elseif (in_array($variables['field_name'], ['created', 'uid', 'title'], TRUE)) {
- // Display created, uid and title fields inline because they will be
- // displayed inline by node.html.twig. Skip this if the field
- // display is configurable and skipping has been enabled.
- // @todo Delete as part of https://www.drupal.org/node/3015623
-
- /** @var \Drupal\node\NodeInterface $node */
- $node = $variables['element']['#object'];
- $skip_custom_preprocessing = $node->getEntityType()->get('enable_base_field_custom_preprocess_skipping');
- $variables['is_inline'] = !$skip_custom_preprocessing || !$node->getFieldDefinition($variables['field_name'])->isDisplayConfigurable('view');
- }
-}
-
-/**
* Implements hook_theme_suggestions_HOOK().
*/
function node_theme_suggestions_node(array $variables): array {
diff --git a/core/modules/node/src/Hook/NodeThemeHooks.php b/core/modules/node/src/Hook/NodeThemeHooks.php
new file mode 100644
index 000000000000..7ee443c458f8
--- /dev/null
+++ b/core/modules/node/src/Hook/NodeThemeHooks.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\node\Hook;
+
+use Drupal\Core\Hook\Attribute\Preprocess;
+
+/**
+ * Hook implementations for the node module.
+ */
+class NodeThemeHooks {
+
+ /**
+ * Implements hook_preprocess_HOOK() for node field templates.
+ */
+ #[Preprocess('field__node')]
+ public function preprocessFieldNode(&$variables): void {
+ // Set a variable 'is_inline' in cases where inline markup is required,
+ // without any block elements such as <div>.
+ if ($variables['element']['#is_page_title'] ?? FALSE) {
+ // Page title is always inline because it will be displayed inside <h1>.
+ $variables['is_inline'] = TRUE;
+ }
+ elseif (in_array($variables['field_name'], ['created', 'uid', 'title'], TRUE)) {
+ // Display created, uid and title fields inline because they will be
+ // displayed inline by node.html.twig. Skip this if the field
+ // display is configurable and skipping has been enabled.
+ // @todo Delete as part of https://www.drupal.org/node/3015623
+
+ /** @var \Drupal\node\NodeInterface $node */
+ $node = $variables['element']['#object'];
+ $skip_custom_preprocessing = $node->getEntityType()->get('enable_base_field_custom_preprocess_skipping');
+ $variables['is_inline'] = !$skip_custom_preprocessing || !$node->getFieldDefinition($variables['field_name'])->isDisplayConfigurable('view');
+ }
+ }
+
+}
diff --git a/core/modules/options/options.services.yml b/core/modules/options/options.services.yml
index b3734c763b1d..dbce14350f80 100644
--- a/core/modules/options/options.services.yml
+++ b/core/modules/options/options.services.yml
@@ -1,2 +1,2 @@
parameters:
- options.hooks_converted: true
+ options.skip_procedural_hook_scan: true
diff --git a/core/modules/path/path.services.yml b/core/modules/path/path.services.yml
index c500d12fbeea..ce638d5e3ede 100644
--- a/core/modules/path/path.services.yml
+++ b/core/modules/path/path.services.yml
@@ -1,2 +1,2 @@
parameters:
- path.hooks_converted: true
+ path.skip_procedural_hook_scan: true
diff --git a/core/modules/responsive_image/responsive_image.services.yml b/core/modules/responsive_image/responsive_image.services.yml
index c382d2567225..a145f1dad9e1 100644
--- a/core/modules/responsive_image/responsive_image.services.yml
+++ b/core/modules/responsive_image/responsive_image.services.yml
@@ -1,2 +1,2 @@
parameters:
- responsive_image.hooks_converted: true
+ responsive_image.skip_procedural_hook_scan: false
diff --git a/core/modules/rest/rest.services.yml b/core/modules/rest/rest.services.yml
index aa249ed37825..cb19a18e1ae7 100644
--- a/core/modules/rest/rest.services.yml
+++ b/core/modules/rest/rest.services.yml
@@ -1,5 +1,5 @@
parameters:
- rest.hooks_converted: true
+ rest.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml
index 9dc3a849ce9a..9629e55d6520 100644
--- a/core/modules/serialization/serialization.services.yml
+++ b/core/modules/serialization/serialization.services.yml
@@ -1,5 +1,5 @@
parameters:
- serialization.hooks_converted: true
+ serialization.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/settings_tray/settings_tray.services.yml b/core/modules/settings_tray/settings_tray.services.yml
index b09ec37f1aff..8ff9184774e9 100644
--- a/core/modules/settings_tray/settings_tray.services.yml
+++ b/core/modules/settings_tray/settings_tray.services.yml
@@ -1,5 +1,5 @@
parameters:
- settings_tray.hooks_converted: true
+ settings_tray.skip_procedural_hook_scan: false
services:
_defaults:
diff --git a/core/modules/shortcut/shortcut.services.yml b/core/modules/shortcut/shortcut.services.yml
index 00671a667b53..fdd4bc85c217 100644
--- a/core/modules/shortcut/shortcut.services.yml
+++ b/core/modules/shortcut/shortcut.services.yml
@@ -1,5 +1,5 @@
parameters:
- shortcut.hooks_converted: true
+ shortcut.skip_procedural_hook_scan: false
services:
_defaults:
diff --git a/core/modules/syslog/syslog.services.yml b/core/modules/syslog/syslog.services.yml
index 3aaa962709cc..8892ab797eeb 100644
--- a/core/modules/syslog/syslog.services.yml
+++ b/core/modules/syslog/syslog.services.yml
@@ -1,5 +1,5 @@
parameters:
- syslog.hooks_converted: true
+ syslog.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/modules/system/tests/modules/hook_collector_skip_procedural/hook_collector_skip_procedural.services.yml b/core/modules/system/tests/modules/hook_collector_skip_procedural/hook_collector_skip_procedural.services.yml
index 205fd7c2b670..2309835abaac 100644
--- a/core/modules/system/tests/modules/hook_collector_skip_procedural/hook_collector_skip_procedural.services.yml
+++ b/core/modules/system/tests/modules/hook_collector_skip_procedural/hook_collector_skip_procedural.services.yml
@@ -1,2 +1,2 @@
parameters:
- hook_collector_skip_procedural.hooks_converted: true
+ hook_collector_skip_procedural.skip_procedural_hook_scan: true
diff --git a/core/modules/system/tests/modules/hook_collector_skip_procedural_attribute/hook_collector_skip_procedural_attribute.module b/core/modules/system/tests/modules/hook_collector_skip_procedural_attribute/hook_collector_skip_procedural_attribute.module
index e7b4bf3f6829..31bc606d37e0 100644
--- a/core/modules/system/tests/modules/hook_collector_skip_procedural_attribute/hook_collector_skip_procedural_attribute.module
+++ b/core/modules/system/tests/modules/hook_collector_skip_procedural_attribute/hook_collector_skip_procedural_attribute.module
@@ -7,7 +7,7 @@
declare(strict_types=1);
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
/**
* This implements a hook and should be picked up.
@@ -25,7 +25,7 @@ function hook_collector_skip_procedural_attribute_cache_flush(): void {
* This attribute should stop all procedural hooks after.
* We implement on behalf of other modules so we can pick them up.
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function hook_collector_on_behalf_procedural_cache_flush(): void {
// Set a global value we can check in test code.
$GLOBALS['procedural_attribute_skip_has_attribute'] = 'procedural_attribute_skip_has_attribute';
diff --git a/core/modules/system/tests/modules/module_test_oop_preprocess/module_test_oop_preprocess.info.yml b/core/modules/system/tests/modules/module_test_oop_preprocess/module_test_oop_preprocess.info.yml
new file mode 100644
index 000000000000..c3d2e98f6f46
--- /dev/null
+++ b/core/modules/system/tests/modules/module_test_oop_preprocess/module_test_oop_preprocess.info.yml
@@ -0,0 +1,5 @@
+name: test oop preprocess functions
+type: module
+description: 'Test module used to test oop preprocess hooks are executed.'
+package: Testing
+version: VERSION
diff --git a/core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php b/core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php
new file mode 100644
index 000000000000..1cbb9e6b422b
--- /dev/null
+++ b/core/modules/system/tests/modules/module_test_oop_preprocess/src/Hook/ModuleTestOopPreprocessThemeHooks.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\module_test_oop_preprocess\Hook;
+
+use Drupal\Core\Hook\Attribute\Preprocess;
+
+/**
+ * Hook implementations for module_test_oop_preprocess.
+ */
+class ModuleTestOopPreprocessThemeHooks {
+
+ #[Preprocess]
+ public function rootPreprocess($arg): mixed {
+ return $arg;
+ }
+
+ #[Preprocess('test')]
+ public function preprocessTest($arg): mixed {
+ return $arg;
+ }
+
+}
diff --git a/core/modules/system/tests/modules/module_test_procedural_preprocess/module_test_procedural_preprocess.info.yml b/core/modules/system/tests/modules/module_test_procedural_preprocess/module_test_procedural_preprocess.info.yml
new file mode 100644
index 000000000000..2cc1e7840d5d
--- /dev/null
+++ b/core/modules/system/tests/modules/module_test_procedural_preprocess/module_test_procedural_preprocess.info.yml
@@ -0,0 +1,5 @@
+name: test procedural preprocess functions
+type: module
+description: 'Test module used to test procedural preprocess hooks are executed.'
+package: Testing
+version: VERSION
diff --git a/core/modules/system/tests/modules/module_test_procedural_preprocess/module_test_procedural_preprocess.module b/core/modules/system/tests/modules/module_test_procedural_preprocess/module_test_procedural_preprocess.module
new file mode 100644
index 000000000000..1e7665b412d0
--- /dev/null
+++ b/core/modules/system/tests/modules/module_test_procedural_preprocess/module_test_procedural_preprocess.module
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @file
+ * Test module.
+ */
+
+declare(strict_types=1);
+
+function template_preprocess_test($arg): mixed {
+ return $arg;
+}
+
+function module_test_procedural_preprocess_preprocess($arg): mixed {
+ return $arg;
+}
+
+function module_test_procedural_preprocess_preprocess_test($arg): mixed {
+ return $arg;
+}
diff --git a/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php b/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php
new file mode 100644
index 000000000000..fc48756de51a
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_test/src/Hook/ThemeTestThemeHooks.php
@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\theme_test\Hook;
+
+use Drupal\Core\Hook\Attribute\Preprocess;
+
+/**
+ * Hook implementations for theme_test.
+ */
+class ThemeTestThemeHooks {
+
+ /**
+ * Implements hook_preprocess_HOOK().
+ */
+ #[Preprocess('theme_test_preprocess_suggestions__monkey')]
+ public function preprocessTestSuggestions(&$variables): void {
+ $variables['foo'] = 'Monkey';
+ }
+
+}
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module
index aabef2b6c4aa..72c92352b617 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.module
+++ b/core/modules/system/tests/modules/theme_test/theme_test.module
@@ -32,13 +32,6 @@ function theme_test_preprocess_theme_test_preprocess_suggestions(&$variables): v
}
/**
- * Tests a module overriding a default hook with a suggestion.
- */
-function theme_test_preprocess_theme_test_preprocess_suggestions__monkey(&$variables): void {
- $variables['foo'] = 'Monkey';
-}
-
-/**
* Prepares variables for test render element templates.
*
* Default template: theme-test-render-element.html.twig.
diff --git a/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php b/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php
index eccaceaeca70..5520b49f762e 100644
--- a/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php
+++ b/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php
@@ -362,6 +362,56 @@ class ModuleHandlerTest extends KernelTestBase {
}
/**
+ * Tests procedural preprocess functions.
+ */
+ public function testProceduralPreprocess(): void {
+ $this->moduleInstaller()->install(['module_test_procedural_preprocess']);
+ $preprocess_function = [];
+ $preprocess_invoke = [];
+ $prefix = 'module_test_procedural_preprocess';
+ $hook = 'test';
+ if ($this->moduleHandler()->hasImplementations('preprocess', [$prefix], TRUE)) {
+ $function = "{$prefix}_preprocess";
+ $preprocess_function[] = $function;
+ $preprocess_invoke[$function] = ['module' => $prefix, 'hook' => 'preprocess'];
+ }
+ if ($this->moduleHandler()->hasImplementations('preprocess_' . $hook, [$prefix], TRUE)) {
+ $function = "{$prefix}_preprocess_{$hook}";
+ $preprocess_function[] = $function;
+ $preprocess_invoke[$function] = ['module' => $prefix, 'hook' => 'preprocess_' . $hook];
+ }
+
+ foreach ($preprocess_function as $function) {
+ $this->assertTrue($this->moduleHandler()->invoke(... $preprocess_invoke[$function], args: [TRUE]), 'Procedural hook_preprocess runs.');
+ }
+ }
+
+ /**
+ * Tests Oop preprocess functions.
+ */
+ public function testOopPreprocess(): void {
+ $this->moduleInstaller()->install(['module_test_oop_preprocess']);
+ $preprocess_function = [];
+ $preprocess_invoke = [];
+ $prefix = 'module_test_oop_preprocess';
+ $hook = 'test';
+ if ($this->moduleHandler()->hasImplementations('preprocess', [$prefix], TRUE)) {
+ $function = "{$prefix}_preprocess";
+ $preprocess_function[] = $function;
+ $preprocess_invoke[$function] = ['module' => $prefix, 'hook' => 'preprocess'];
+ }
+ if ($this->moduleHandler()->hasImplementations('preprocess_' . $hook, [$prefix], TRUE)) {
+ $function = "{$prefix}_preprocess_{$hook}";
+ $preprocess_function[] = $function;
+ $preprocess_invoke[$function] = ['module' => $prefix, 'hook' => 'preprocess_' . $hook];
+ }
+
+ foreach ($preprocess_function as $function) {
+ $this->assertTrue($this->moduleHandler()->invoke(... $preprocess_invoke[$function], args: [TRUE]), 'Procedural hook_preprocess runs.');
+ }
+ }
+
+ /**
* Returns the ModuleHandler.
*
* @return \Drupal\Core\Extension\ModuleHandlerInterface
diff --git a/core/modules/text/text.services.yml b/core/modules/text/text.services.yml
index 207641245768..720e0fe820b7 100644
--- a/core/modules/text/text.services.yml
+++ b/core/modules/text/text.services.yml
@@ -1,2 +1,2 @@
parameters:
- text.hooks_converted: true
+ text.skip_procedural_hook_scan: true
diff --git a/core/modules/toolbar/toolbar.services.yml b/core/modules/toolbar/toolbar.services.yml
index 238520267207..67a1b0719165 100644
--- a/core/modules/toolbar/toolbar.services.yml
+++ b/core/modules/toolbar/toolbar.services.yml
@@ -1,5 +1,5 @@
parameters:
- toolbar.hooks_converted: true
+ toolbar.skip_procedural_hook_scan: false
services:
_defaults:
diff --git a/core/modules/update/update.authorize.inc b/core/modules/update/update.authorize.inc
index 8a2ba55d667f..64f8fe7b014a 100644
--- a/core/modules/update/update.authorize.inc
+++ b/core/modules/update/update.authorize.inc
@@ -5,7 +5,7 @@
*/
use Drupal\Core\Batch\BatchBuilder;
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\Updater\UpdaterException;
use Drupal\Core\Url;
@@ -31,7 +31,7 @@ use Drupal\Core\Url;
* an instance of \Symfony\Component\HttpFoundation\Response the calling code
* should use that response for the current page request.
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function update_authorize_run_update($filetransfer, $projects) {
@trigger_error(__METHOD__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no replacement. Use composer to manage the code for your site. See https://www.drupal.org/node/3512364', E_USER_DEPRECATED);
diff --git a/core/modules/update/update.compare.inc b/core/modules/update/update.compare.inc
index 1ea781a974b6..05c6565ce197 100644
--- a/core/modules/update/update.compare.inc
+++ b/core/modules/update/update.compare.inc
@@ -5,7 +5,7 @@
*/
use Drupal\Core\Extension\ExtensionVersion;
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\Utility\Error;
use Drupal\update\ProjectRelease;
use Drupal\update\UpdateFetcherInterface;
@@ -23,7 +23,7 @@ use Drupal\update\ProjectCoreCompatibility;
* Array of project information from
* \Drupal\update\UpdateManager::getProjects().
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function update_process_project_info(&$projects): void {
foreach ($projects as $key => $project) {
// Assume an official release until we see otherwise.
diff --git a/core/modules/update/update.fetch.inc b/core/modules/update/update.fetch.inc
index 2c828cc3e7fb..296dfbd3d06d 100644
--- a/core/modules/update/update.fetch.inc
+++ b/core/modules/update/update.fetch.inc
@@ -4,7 +4,7 @@
* @file
*/
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\update\UpdateManagerInterface;
/**
@@ -16,7 +16,7 @@ use Drupal\update\UpdateManagerInterface;
*
* @see update_requirements()
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function _update_cron_notify(): void {
$update_config = \Drupal::config('update.settings');
\Drupal::moduleHandler()->loadInclude('update', 'install');
diff --git a/core/modules/update/update.install b/core/modules/update/update.install
index 2981ea91373a..3ad55265aab1 100644
--- a/core/modules/update/update.install
+++ b/core/modules/update/update.install
@@ -5,7 +5,7 @@
* Install, update, and uninstall functions for the Update Status module.
*/
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\update\ProjectSecurityData;
@@ -74,7 +74,7 @@ function update_requirements($phase): array {
/**
* Implements hook_install().
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function update_install(): void {
$queue = \Drupal::queue('update_fetch_tasks', TRUE);
$queue->createQueue();
diff --git a/core/modules/update/update.manager.inc b/core/modules/update/update.manager.inc
index 46611f85cd87..f4b18321002b 100644
--- a/core/modules/update/update.manager.inc
+++ b/core/modules/update/update.manager.inc
@@ -7,7 +7,7 @@
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\Exception\InvalidStreamWrapperException;
use Drupal\Core\File\FileExists;
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Psr\Http\Client\ClientExceptionInterface;
/**
@@ -24,7 +24,7 @@ use Psr\Http\Client\ClientExceptionInterface;
* workflow, or FALSE if we've hit a fatal configuration and must halt the
* workflow.
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function _update_manager_check_backends(&$form, $operation) {
@trigger_error(__METHOD__ . '() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no replacement. Use composer to manage the code for your site. See https://www.drupal.org/node/3512364', E_USER_DEPRECATED);
diff --git a/core/modules/update/update.module b/core/modules/update/update.module
index c4366293a059..e06ac21ea317 100644
--- a/core/modules/update/update.module
+++ b/core/modules/update/update.module
@@ -5,7 +5,7 @@
*/
use Drupal\Core\File\Exception\FileException;
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
+use Drupal\Core\Hook\Attribute\ProceduralHookScanStop;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\Core\Site\Settings;
@@ -15,7 +15,7 @@ use Drupal\update\UpdateManagerInterface;
/**
* Returns a warning message when there is no data about available updates.
*/
-#[StopProceduralHookScan]
+#[ProceduralHookScanStop]
function _update_no_data() {
$destination = \Drupal::destination()->getAsArray();
return t('No update information available. <a href=":run_cron">Run cron</a> or <a href=":check_manually">check manually</a>.', [
diff --git a/core/modules/update/update.report.inc b/core/modules/update/update.report.inc
index 615e54b35b20..75957754bd87 100644
--- a/core/modules/update/update.report.inc
+++ b/core/modules/update/update.report.inc
@@ -4,7 +4,6 @@
* @file
*/
-use Drupal\Core\Hook\Attribute\StopProceduralHookScan;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\update\ProjectRelease;
@@ -20,7 +19,6 @@ use Drupal\update\UpdateManagerInterface;
* An associative array containing:
* - data: An array of data about each project's status.
*/
-#[StopProceduralHookScan]
function template_preprocess_update_report(&$variables): void {
$data = isset($variables['data']) && is_array($variables['data']) ? $variables['data'] : [];
diff --git a/core/modules/workflows/workflows.services.yml b/core/modules/workflows/workflows.services.yml
index 868807dfc639..ad50650e6dbc 100644
--- a/core/modules/workflows/workflows.services.yml
+++ b/core/modules/workflows/workflows.services.yml
@@ -1,5 +1,5 @@
parameters:
- workflows.hooks_converted: true
+ workflows.skip_procedural_hook_scan: true
services:
_defaults:
diff --git a/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php b/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php
index 774f1289ccdb..58b608248c9f 100644
--- a/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Hook/HookCollectorPassTest.php
@@ -143,19 +143,6 @@ class HookCollectorPassTest extends KernelTestBase {
}
/**
- * Test Hook attribute with named arguments, and class with invoke method.
- */
- public function testHookAttribute(): void {
- $module_installer = $this->container->get('module_installer');
- $this->assertTrue($module_installer->install(['hook_collector_hook_attribute']));
- $this->assertFalse(isset($GLOBALS['hook_named_arguments']));
- $this->assertFalse(isset($GLOBALS['hook_invoke_method']));
- drupal_flush_all_caches();
- $this->assertTrue(isset($GLOBALS['hook_named_arguments']));
- $this->assertTrue(isset($GLOBALS['hook_invoke_method']));
- }
-
- /**
* Tests hook ordering with attributes.
*/
public function testHookFirst(): void {
diff --git a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php
index f237c09d1caa..862d7e2f65d8 100644
--- a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php
+++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php
@@ -157,19 +157,21 @@ class RegistryTest extends UnitTestCase {
include_once $this->root . '/core/modules/system/tests/modules/theme_test/theme_test.module';
include_once $this->root . '/core/tests/fixtures/test_stable/test_stable.theme';
$themeTestTheme = new ThemeTestHooks();
- $this->moduleHandler->expects($this->atLeastOnce())
+ $this->moduleHandler->expects($this->exactly(2))
->method('invoke')
->with('theme_test', 'theme')
->willReturn($themeTestTheme->theme(NULL, NULL, NULL, NULL));
- $this->moduleHandler->expects($this->atLeastOnce())
+ $this->moduleHandler->expects($this->atMost(50))
->method('invokeAllWith')
- ->with('theme')
- ->willReturnCallback(function (string $hook, callable $callback) {
- $callback(function () {}, 'theme_test');
- });
- $this->moduleHandler->expects($this->atLeastOnce())
+ // $callback is documented on ModuleHandlerInterface::invokeAllWith().
+ // The first argument expects a callable, but it doesn't matter what it
+ // is, use pi() as a canary in case code changes, and it begins to use it.
+ // The second argument is the module name and for that theme_test is
+ // always correct here.
+ ->willReturnCallback(fn (string $hook, callable $callback) => $callback('pi', 'theme_test'));
+ $this->moduleHandler->expects($this->exactly(2))
->method('getModuleList')
- ->willReturn([]);
+ ->willReturn(['theme_test' => NULL]);
$this->moduleList->expects($this->exactly(2))
->method('getPath')
->with('theme_test')