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

sdjili created an issue. See original summary.

sdjili’s picture

Issue summary: View changes
ghost of drupal past’s picture

Is 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.

nicxvan’s picture

Component: base system » extension system

This is a duplicate, but let me leave it open so I can transfer some content.

The other issue just needs a rebase.

nicxvan’s picture

Status: Active » Closed (duplicate)

I 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.

Now that this issue is closed, please review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, please credit people who helped resolve this issue.