Problem/Motivation
Critical Bug in Drupal 11.2.3 The `HookCollectorPass::collectAllHookImplementations()` method incorrectly matches hook functions when module names overlap (e.g., `module_name` vs `module_name_suffix`). This causes hook implementations to be attributed to the wrong module, leading to hooks not being called.
Steps to reproduce
1. Create two modules with overlapping names:
- Module A: `example_config`
- Module B: `example_config_page`
2. Add a hook implementation in Module A:
// example_config.module
function example_config_page_attachments(array &$page): void {
// Implementation
}
3. Enable both modules
4. Clear caches
5. Load a page that should trigger `hook_page_attachments()`
Expected Behavior
The hook `example_config_page_attachments()` should be:
- Correctly attributed to module `example_config`
- Parsed as module: `example_config`, hook: `page_attachments`
- Successfully called during page rendering
Actual Behavior
The hook `example_config_page_attachments()` is:
- Incorrectly attributed to module `example_config_page`
- Parsed as module: `example_config_page`, hook: `attachments`
- Never called because it's registered under the wrong module
Root Cause Analysis
File: `core/lib/Drupal/Core/Hook/HookCollectorPass.php`
Method: `collectAllHookImplementations()`
Lines: 366-371
The Problem
// Line 366: Modules are sorted by length (longest first)
usort($modules_by_length, static fn ($a, $b) => strlen($b) - strlen($a));
// Line 367-370: Create regex pattern
$known_modules_pattern = implode('|', array_map(
static fn ($x) => preg_quote($x, '/'),
$modules_by_length,
));
// Line 371: Final regex
$module_preg = '/^(?<function>(?<module>' . $known_modules_pattern . ')_(?!update_\d)(?<hook>[a-zA-Z0-9_\x80-\xff]+$))/';
When processing function `example_config_page_attachments`:
1. Expected regex order: `example_config_page|example_config|...`
2. Actual matching: First alternative `example_config_page` matches
3. Wrong result:
- Module: `example_config_page` ✗
- Hook: `attachments` ✗
4. Should be:
- Module: `example_config` ✓
- Hook: `page_attachments` ✓
Proposed resolution
Non-greedy Matching
// Change regex to use more specific word boundary matching
$module_preg = '/^(?<function>(?<module>' . $known_modules_pattern . ')_(?!update_\d)(?<hook>[a-zA-Z0-9_\x80-\xff]+)$)/';
Or using an attribute-based hook system that uses the event dispatcher instead of regex pattern matching. This completely eliminates naming conflicts
// Problematic procedural hook
function my_module_page_attachments(array &$page): void {
$config = \Drupal::config('my_module.settings');
// Hook logic...
}
function my_module_form_alter(array &$form, FormStateInterface $form_state, $form_id): void {
// More hook logic...
}
### After (Modern Attributes)
// Clean, modern hook class
#[Hook('page_attachments')]
public function pageAttachments(array &$page): void {
$config = $this->configFactory->get('my_module.settings');
// Hook logic...
}
#[Hook('form_alter')]
public function formAlter(array &$form, FormStateInterface $form_state, $form_id): void {
// More hook logic...
}
This bug is particularly insidious because:
1. Silent failure - no error messages
2. Intermittent - depends on module loading order
3. Hard to debug - requires deep core knowledge
4. Production impact - can break live sites after module updates
Comments
Comment #2
sdjili commentedComment #3
ghost of drupal pastIs this a duplicate of #3502302: HookCollectorPass fails to register hooks for the correct module in some contexts ? If so, I would recommend closing this and reusing the title in the older issue because this title is much clearer.
Comment #4
nicxvan commentedThis is a duplicate, but let me leave it open so I can transfer some content.
The other issue just needs a rebase.
Comment #5
nicxvan commentedI updated the other issue's title to be a bit better but I don't want to copy this one.
I also called out the excellent summary here.
I'm going to close this as a duplicate now but I did grant credit for the summary clearly showing the issue and outlining where to look.
I'm not sure if you searched for the issue first, but feel free to test the MR in the related issue.