HookCollectorPass registers event listeners but they do not follow the required calling convention

Created on 15 February 2025, 4 months ago

Problem/Motivation

The HookCollectorPass is tagging hook implementations with the kernel.event_listener tag. However, the registered methods (i.e. hook implementations) do not follow the event listener calling convention.

If an event name in custom / contrib code happens to collide with the generated name of a hook registered by the HookCollectorPass, then EventDispatcher::dispatch() will crash when it attempts to invoke the listener because it required arguments will be missing.

Steps to reproduce

use Symfony\Contracts\EventDispatcher\Event;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

\Drupal::service(EventDispatcherInterface::class)->dispatch(new Event(), 'drupal_hook.help');
TypeError: Drupal\announcements_feed\Hook\AnnouncementsFeedHooks::help(): Argument #2 ($route_match) must be of type Drupal\Core\Routing\RouteMatchInterface, string given, called in vendor/symfony/event-dispatcher/EventDispatcher.php on line 246 in core/modules/announcements_feed/src/Hook/AnnouncementsFeedHooks.php on line 19
#0 vendor/symfony/event-dispatcher/EventDispatcher.php(246): Drupal\announcements_feed\Hook\AnnouncementsFeedHooks->help()
#1 vendor/symfony/event-dispatcher/EventDispatcher.php(206): Symfony\Component\EventDispatcher\EventDispatcher::Symfony\Component\EventDispatcher\{closure}()
#2 vendor/symfony/event-dispatcher/EventDispatcher.php(56): Symfony\Component\EventDispatcher\EventDispatcher->callListeners()
#3 event-dispatcher-boom.php(8): Symfony\Component\EventDispatcher\EventDispatcher->dispatch()

Proposed resolution

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

πŸ› Bug report
Status

Active

Version

11.0 πŸ”₯

Component

base system

Created by

πŸ‡¨πŸ‡­Switzerland znerol

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @znerol
  • πŸ‡¨πŸ‡­Switzerland znerol
  • πŸ‡¨πŸ‡­Switzerland znerol
  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    Why aren't you using the documented methods for invoking hooks?
    What is the naming convention you're referring to?

  • πŸ‡¨πŸ‡¦Canada Charlie ChX Negyesi 🍁Canada

    Please check the problem/motivation section of πŸ“Œ OOP hooks using event dispatcher Needs review

  • πŸ‡¨πŸ‡­Switzerland znerol

    I guess I need to clarify this a bit:

    This is not a feature request: I do not wish to invoke a hook via the event dispatcher.

    This is rather a bug report: If some preexisting custom / contrib module defines an event which happens to collide with one of the newly added drupal_hook.* pseudo-events, then that module will break when attempting to dispatch its own event (with a trace similar to the one in the issue summary). This might be BC problem.

    The problem is that EventDispatcher expects a certain method signature for all registered listeners. OOP hook implementations explicitly do not implement that method signature.

    Also should upstream decide to add sanity checks on the methods collected by RegisterListenersPass, then that will break OOP hooks in the future.

    I have the impression that the close coupling of ModuleHandler to EventDispatcher is artificial. It looks like ModuleHandler only wants to reuse getListeners(). That means, it just uses EventDispatcher as a registry.

    It would be easy to implement the registry part of EventDispatcher in ModuleHandler (or in a dedicated service) in order to decouple ModuleHandler entirely from upstream.

  • πŸ‡¨πŸ‡­Switzerland znerol
  • πŸ‡¨πŸ‡­Switzerland znerol
  • πŸ‡¨πŸ‡­Switzerland znerol
  • πŸ‡¨πŸ‡¦Canada Charlie ChX Negyesi 🍁Canada

    Yes, of course, you and I already wrote an event dispatcher in #1972300: Write a more scalable dispatcher β†’ it's not like that's hard to do it was reverted in πŸ“Œ Replace ContainerAwareEventDispatcher with Symfony EventDispatcher Fixed we can unrevert it :D :D

  • πŸ‡¨πŸ‡­Switzerland znerol

    we can unrevert it :D :D

    That wouldn't resolve the bug, though. The hook registry needs to be separate from EventDispatcher.

    The implementation from #1972300: Write a more scalable dispatcher β†’ could certainly be reused for ModuleHandler.

  • πŸ‡¨πŸ‡¦Canada Charlie ChX Negyesi 🍁Canada

    Of course, of course, that's what I meant. https://www.drupal.org/project/drupal/issues/3485896 πŸ“Œ Hook ordering across OOP, procedural and with extra types Active is first so people don't try to order via event listener priorities.

    Truth to be told, I only kept the event dispatcher because it's easier to get changes in if they use Symfony. It's truth. I wish I was kidding but I am not.

  • First commit to issue fork.
  • πŸ‡©πŸ‡ͺGermany donquixote

    First attempt, including changes from πŸ“Œ [pp-1] Explore adding an ImplementationList object for hook implmentations Active

  • πŸ‡©πŸ‡ͺGermany donquixote

    Some things should be discussed further:
    - Breaking changes in ModuleHandler constructor parameter signature.
    - Whether the [$hook => ["$class::$method" => $module]] structure is a good idea for the parameter.

  • Pipeline finished with Failed
    about 1 month ago
    Total: 400s
    #485246
  • Pipeline finished with Failed
    about 1 month ago
    #485249
  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    This should probably be postponed on the other issue since this is built on top of it.

    Adding framework tag too.

    I want to do a deeper review if the other issue first but one question on the mr.

    As for changing the signature, high level process:

    Add both types to the parameters that change.
    In the constructor throw a deprecation message if it is the type being removed and replace the property with the new type.

    A combination of https://www.drupal.org/about/core/policies/core-change-policies/how-to-d... β†’ and https://www.drupal.org/about/core/policies/core-change-policies/how-to-d... β†’

  • Pipeline finished with Failed
    about 1 month ago
    #486966
  • Pipeline finished with Failed
    about 1 month ago
    Total: 258s
    #486976
  • Pipeline finished with Failed
    about 1 month ago
    #486978
  • Pipeline finished with Failed
    about 1 month ago
    Total: 196s
    #487418
  • Pipeline finished with Failed
    about 1 month ago
    Total: 194s
    #487434
  • Pipeline finished with Failed
    about 1 month ago
    Total: 230s
    #487513
  • Pipeline finished with Failed
    about 1 month ago
    Total: 362s
    #487529
  • Pipeline finished with Failed
    about 1 month ago
    Total: 248s
    #487809
  • Pipeline finished with Failed
    about 1 month ago
    #488093
  • Pipeline finished with Failed
    about 1 month ago
    Total: 790s
    #488095
  • Pipeline finished with Failed
    about 1 month ago
    Total: 742s
    #488171
  • Pipeline finished with Failed
    about 1 month ago
    Total: 233s
    #488249
  • Pipeline finished with Failed
    about 1 month ago
    Total: 230s
    #488251
  • Pipeline finished with Failed
    about 1 month ago
    #488255
  • Pipeline finished with Failed
    about 1 month ago
    Total: 683s
    #488259
  • Pipeline finished with Success
    about 1 month ago
    Total: 689s
    #488405
  • πŸ‡©πŸ‡ͺGermany donquixote

    It's green!
    Now it is time for the BC police.
    I am changing the signature of ModuleHandler, we need to see how we can make this BC friendly.

  • πŸ‡©πŸ‡ͺGermany donquixote

    Actually one round of review feedback could be nice.

  • Pipeline finished with Success
    about 1 month ago
    Total: 572s
    #488458
  • πŸ‡©πŸ‡ͺGermany donquixote

    I discussed with @ghost of drupal past in slack about the ModuleHandler constructor BC support.

    Technically it could be possible to do it, but it would require some advanced acrobatics to reconstruct the list.
    In 11.0.x -> 11.1.x we already broke that signature once.

    Our current consensus is to break it again, without BC support for custom or contrib code that extends ModuleHandler.

    We only need to make sure that you can successfully run container rebuild (drush cr, or by other means?) after code update.

  • Pipeline finished with Failed
    about 1 month ago
    Total: 156s
    #490172
  • πŸ‡©πŸ‡ͺGermany donquixote

    All review points have been addressed.

    But we should do the following issues first to start at a good place:
    πŸ“Œ Enhance array docs in ModuleHandler Active
    πŸ› ModuleHandler::resetImplementations should reset $this->invokeMap Active

  • Pipeline finished with Failed
    about 1 month ago
    Total: 239s
    #490190
  • Pipeline finished with Success
    about 1 month ago
    Total: 456s
    #490199
  • πŸ‡©πŸ‡ͺGermany donquixote

    The ProceduralCall class is gone.
    (TBD if we want to keep it for BC, or do that removal in a follow-up)

    Actually I would be fine to do this in a follow-up.
    When we do this, we also want to review the ordering attributes, where currently you would do #[ReOrderHook(ProceduralCall::class, 'foo', ..)]. See πŸ“Œ [pp-1] Create new attribute to make reordering procedural hooks easier. Postponed

  • πŸ‡ΊπŸ‡ΈUnited States nicxvan
  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    How do we interact with the procedural calls if that class is gone?

  • πŸ‡©πŸ‡ͺGermany donquixote

    How do we interact with the procedural calls if that class is gone?

    I assume this is about "ProceduralCall" class.
    That class is already marked as internal, so using it to target a hook would already be wrong.

    A solution for this can be discussed in πŸ“Œ [pp-1] Create new attribute to make reordering procedural hooks easier. Postponed .

  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    Using the proceduralcall class is not wrong. Internal is just a warning things could change.

    We cannot remove it without providing an alternative way to target procedural hooks for ordering.

  • πŸ‡©πŸ‡ͺGermany donquixote

    We cannot remove it without providing an alternative way to target procedural hooks for ordering.

    In the other issue the argument was made that it is usually fine to just target all implementations of the respective module.

    As a reminder, there were two cases where we really need to target just the procedural implementation:

    • The target module has procedural _and_ OOP implementations for the same hook.
      This could be because the OOP implementations were added later, and provide different functionality, while the old procedural hook implementation has not been converted yet (because out of scope/budget).
    • We want to support different versions of the target module.
      One version has a single procedural implementation for a given hook. The other version has multiple OOP implementations. We want to target the procedural implementation of the older version, and possible some (but not all) of the OOP implementations from the new version.
  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    We would need to add a way to target a module for Reorder and Remove because both of those target single implementations.

    Can't we move the proceduralcall removal to a followup? It looks like it's not necessary here and is scope creep.

    This change feels far more important.

  • Pipeline finished with Success
    12 days ago
    Total: 1762s
    #506864
  • πŸ‡©πŸ‡ͺGermany donquixote

    I added support for using ProceduralCall in order attributes.

    We still would need tests for this.
    But, tbh, ProceduralCall is marked as @internal, and I don't really want anybody to use it.

    Can't we move the proceduralcall removal to a followup? It looks like it's not necessary here and is scope creep.

    I was proposing the same earlier.
    But it really hurts to see these "Drupal\Core\Extension\ProceduralCall::$function" identifiers, or to see $function passed as a method.
    The class was necessary as long as we had to register hook implementations as event subscribers.

    The complexity of an MR comes from the diff size, but also from the end result.
    In this case, we get a cleaner end result by having a slightly larger diff size (but still less than some earlier MRs).
    I don't want to engineer the new solution around this leftover.

  • πŸ‡©πŸ‡ͺGermany donquixote

    In fact, if we want to reduce the size of this MR, we should get this in first:
    πŸ“Œ [pp-1] Explore adding an ImplementationList object for hook implmentations Active

    This change feels far more important.

    And in general, we should prioritize changes that feel less important, if that makes other changes easier.

  • Pipeline finished with Success
    12 days ago
    Total: 664s
    #506890
  • Pipeline finished with Failed
    3 days ago
    Total: 228s
    #514685
  • πŸ‡©πŸ‡ͺGermany donquixote

    I created an alternative MR where ProceduralCall is still used in hook implementation identifiers and in the parameters passed to ImplementationList::load().
    Tbh, this feels out of place, I don't like it.

  • Pipeline finished with Failed
    3 days ago
    Total: 197s
    #514699
  • Pipeline finished with Failed
    3 days ago
    Total: 362s
    #514700
  • Pipeline finished with Failed
    3 days ago
    Total: 238s
    #514704
  • Pipeline finished with Success
    3 days ago
    Total: 1004s
    #514705
  • Pipeline finished with Success
    2 days ago
    Total: 755s
    #515552
  • Pipeline finished with Success
    2 days ago
    Total: 1075s
    #515553
Production build 0.71.5 2024