Pass the hook as last argument to method

Created on 30 January 2025, 4 months ago

Problem/Motivation

Let's take this hooks implementation method:

  #[Hook('entity_insert')]
  #[Hook('entity_update')]
  #[Hook('entity_delete')]
  #[Hook('entity_translation_insert')]
  #[Hook('entity_translation_delete')]
  #[Hook('entity_revision_delete')]
  public function do(EntityInterface $entity): void {
    ...
  }

The hooks are grouped in a single method because 99% of their code is the same. However, there might be 1% that would depend on the actual invoked hook. To accommodate this I will need to create 6 methods, one for each hook and a 7th method to hold the common code.

It would be good if the method would know which hook was actually invoked. Instead of 7 methods I will write only one.

Proposed resolution

Pass the invoked hook as latest argument to each hook implementation method. Some methods will not use it, they will just omit it from the hook implementation method signature. Others, like in the above use-case, can declare it in the signature and use it:

public function do(EntityInterface $entity, string $hook): void {
  ...
}

For instance, this could be done here, where we append the hook value to $args:

  public function invoke($module, $hook, array $args = []) {
    if ($listeners = $this->getHookListeners($hook)[$module] ?? []) {
      if (count($listeners) > 1) {
        throw new \LogicException("Module $module should not implement $hook more than once");
      }
      $args[] = $hook;
      return reset($listeners)(... $args);
    }

    return $this->legacyInvoke($module, $hook, $args);
  }

Remaining tasks

Discuss the opportunity.

User interface changes

None.

Introduced terminology

N/A

API changes

Each hook implementation method will receive also the hook as last argument.

Data model changes

None.

Release notes snippet

Hook implementation methods will receive the invoked hook as last method argument.

✨ Feature request
Status

Active

Version

11.0 πŸ”₯

Component

base system

Created by

πŸ‡·πŸ‡΄Romania claudiu.cristea Arad πŸ‡·πŸ‡΄

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

Merge Requests

Comments & Activities

  • Issue created by @claudiu.cristea
  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    I was also thinking about this at some point, some hooks already kind of have this feature, such as form alters that pass in $form_id.

    But I'm not sure this this will really result in better code and should be encouraged. Less lines of code is only one thing to consider, more conditional code also increases complexity and is more likely to have bugs.

    I think it would be worth to look at some real-world use cases of this, the example here is obviously made up (you would not want to implement translation and regular entity update/insert hooks at the same time, since you would be called multiple times on the same save for example), the only 1% difference part also seems a bit extreme ;)

  • Merge request !11066Resolve #3503457 "Pass the hook" β†’ (Open) created by claudiu.cristea
  • πŸ‡·πŸ‡΄Romania claudiu.cristea Arad πŸ‡·πŸ‡΄

    (...) the example here is obviously made up (you would not want to implement translation and regular entity update/insert hooks at the same time,

    The only hook made up is entity_translation_insert. My hope was to be able to write something like:

      #[Hook('entity_insert')]
      #[Hook('entity_update')]
      #[Hook('entity_delete')]
      #[Hook('entity_translation_delete')]
      #[Hook('entity_revision_delete')]
      public function do(EntityInterface $entity, str): void {
        $op = match($hook) {
           ...
        };
       \Drupal::service(...)->do($entity, $op);
      }
    
  • Pipeline finished with Success
    4 months ago
    Total: 631s
    #410696
  • πŸ‡¨πŸ‡­Switzerland berdir Switzerland

    Yeah, that example implies that there's some sort of grouped mapping. e.g. insert/update would be one method/parameter on your service and the 3 delete hooks another. I don't think that's better than two methods.

    We're also discussing to remove the coding standard for the Implements hook_foo(), further reducing boilerplate code.

    I very much expect the tests will hit some edge cases. For example, there are hooks with optional parameters, such as hook_entity_field_access(). this isn't going to work with that.

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

    It's also essentially dynamically changing the signature of all hooks.

    Further, invoke is not the most common way of invoking hooks, you'd have to handle invokeAll and invokeAllWith too.

    I think the minute edge case isn't worth the risk and complication here.

    Further the method would be more complex to implement and harder to grok.

    I really think multiple methods with a helper would be the way to go.

  • πŸ‡¬πŸ‡§United Kingdom longwave UK

    -1 for the proposal as is, for the reasons already given. Overloading existing hooks with an extra optional argument just feels like it is going to run into edge cases.

    I think this is clearer with an adapter for each hook implementation that calls a single helper method.

    If there are enough use cases for it I could perhaps get behind an alternative method signature, something like

    #[Hooks(['entity_insert', 'entity_update', 'entity_delete'])]
    public function do(string $hook, ...$args) {
    ...
    }
    

    ie. the hook gets passed in first - but I'm not convinced this is needed enough to do this. It feels like a step back to Drupal 6 and earlier when we had hooks that took $op arguments.

  • πŸ‡¬πŸ‡§United Kingdom longwave UK

    I suppose if you really wanted it to be magic we could use reflection and pass in the hook name as in #7 if the first parameter is string $hook but that would probably be too surprising and not really discoverable.

  • πŸ‡©πŸ‡ͺGermany donquixote

    Always passing the hook as last parameter is problemetic in multiple ways:
    - Variadic hooks that accept any number of arguments.
    - Hooks with many optional arguments.
    - Hooks where we add/invent new parameters in the future.

    An alternative is to mark one parameter (the first one) with a special attribute.
    Or mark the method itself with an attribute.

    class MyHooks {
      #[Hook(...)]
      #[Hook(...)]
      public function foo(#[HookName] string $hook, $other_arg) {}
    }
    
    class MyHooks {
      #[Hook(...)]
      #[Hook(...)]
      #[HookNameAsFirstArgument]
      public function foo(string $hook, $other_arg) {}
    }
    

    I think to remember a case where this would have been convenient, but in the end I did not really need.

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

    I still think this is the wrong way to do it, it's cleaner to just have a helper and pass the caller manually to the helper.

    The situations @donquixote mentioned are just some of the issues.

  • πŸ‡ΊπŸ‡ΈUnited States nicxvan
Production build 0.71.5 2024