Change record status: 
Project: 
Introduced in branch: 
11.1.x
Introduced in version: 
11.1.0
Description: 
  1. Create a class in the Drupal\modulename\Hook namespace (or subdirectory). This will be automatically registered as an autowired service.
  2. Use the new Drupal\Core\Hook\Attribute\Hook attribute either on methods or on the class. If it is on the class and the class doesn't have an __invoke method then a method argument is required.

The method implementing the hook has the same signature as the procedural counterpart.

A very simple example:

function node_user_cancel($edit, UserInterface $account, $method) {
  // DO STUFF
}

after


use Drupal\Core\Hook\Attribute\Hook;
use Drupal\user\UserInterface;

class NodeHooks {

  #[Hook('user_cancel')]
  public function userCancel($edit, UserInterface $account, $method) {
    // DO STUFF
  }

}

While classic hook implementations relied on a magic function name, now the class and method can have any name.

A method can have multiple Hook attributes if it implements multiple hooks. For example, node_comment_insert and node_comment_update have the exact same implementation and so they could become

  #[Hook('comment_insert')]
  #[Hook('comment_update')]
  public function commentInsertOrUpdate(CommentInterface $comment) {

Also, a module can implement almost all hooks multiple times as documented on the Hook attribute itself.

Deprecations

The following methods on ModuleHandler have been deprecated, without replacement. These are removed in Drupal 12.

  • ModuleHandler::getHookInfo()
  • ModuleHandler::writeCache()

Backwards-compatible Hook implementation for Drupal versions from 10.1 to 11.0

Contrib modules which want to adopt this new approach to hooks and continue to support Drupal 10.1 and 11.0 are recommended to implement a new hook class, manually register that class as a service, and add a shim procedural implementation:

node.module

use Drupal\Core\Hook\Attribute\LegacyHook;
use Drupal\node\Hook\NodeHooks;

// @phpstan-ignore-next-line
#[LegacyHook]
function node_user_cancel($edit, UserInterface $account, $method) {
  // This only needs to be returned if the hook previously had a return.
  return \Drupal::service(NodeHooks::class)->userCancel($edit, $account, $method);
}

node.services.yml

services:
  Drupal\node\Hook\NodeHooks:
    class: Drupal\node\Hook\NodeHooks
    autowire: true

src/Hook/NodeHooks.php


namespace Drupal\node\Hook;

use Drupal\Core\Hook\Attribute\Hook;
use Drupal\user\UserInterface;

class NodeHooks {

  #[Hook('user_cancel')]
  public function userCancel($edit, UserInterface $account, $method) {
     // User cancellation processing.
  }

}

With this approach, Drupal versions prior to this change (from 10.1 to 11.0) will call node_user_cancel, which in turn will call the new hook, and the LegacyHook attribute is ignored. Versions of Drupal from 11.1 will not call node_user_cancel because it is marked with the #[LegacyHook] attribute; instead, the new implementation will be called directly. This approach only supports Drupal 10.1 and later because in 10.1 aliases were added to core services for autowiring. The LegacyHook attribute class is not required for this to work (PHP ignores it), but for certain tools it is still better to include it. There is a Rector rule for making all these changes.

Since the attribute does not exist in all versions of Drupal it is recommended that you add a local phpstan ignore rule for each #[LegacyHook] attribute.

Once a contrib module converts all hooks to be OOP a "hooks_converted: true" parameter can be set in the container configuration to improve performance.

In a later Drupal version (Drupal 13 at the earliest), we expect that support for procedural hooks will be removed, at which time these services and the LegacyHook shims will need to be removed as well.

Breaking changes

Conditionally defined hook implementations are not supported.

if ($foo) {
  function foo_hook() {
  }
}

Update: if it is really badly necessary to dynamically define hooks, this issue comment explains how to do it.

Also, any classes extending ModuleHandler are broken. As the class has been rewritten from ground up, any classes relying on the internals of it will not work and a code review is forced by the changed constructor. Once again, such code should be extremely rare. Consider rewriting it as a service decorator.

Update: Since hook_module_implements_alter is called during build time, it does not support service calls. Also, dynamic ordering in general is extremely unlikely to be supported in the future.

Hooks that have had support added in follow up issues

Note that the following list is capturing the situation when this document was created. As the conversion of some hooks from this list is an ongoing process, you can check updates in the Follow-up issues converting hooks to OOP section, below.

Hooks called by ModuleInstaller

  • hook_cache_flush()
  • hook_module_preinstall()
  • hook_module_preuninstall()
  • hook_modules_installed()
  • hook_modules_uninstalled()

These can be converted in modules.

  • hook_theme_suggestion_HOOK()
  • hook_theme_suggestions_HOOK_alter()

Hooks that remain procedural

Legacy meta hooks

  • hook_hook_info()
  • hook_module_implements_alter()

Install/Update hooks

  • hook_install()
  • hook_install_tasks()
  • hook_install_tasks_alter()
  • hook_post_update_NAME()
  • hook_removed_post_updates()
  • hook_requirements()
  • hook_schema()
  • hook_uninstall()
  • hook_update_dependencies()
  • hook_update_last_removed()
  • hook_update_N()

All of these are being looked at but for now they remain procedural.

Theme hooks

  • hook_preprocess_HOOK()
  • hooks implemented BY themes
  • Alter hooks implemented BY themes
  • Hooks implemented by modules on BEHALF of a theme

Uppercase HOOK means a theme hook. (This is not at all confusing.) hook_theme() is INCLUDED in this change, it is not a theme hook. The registry explicitly calls moduleHandler:

$result = $this->moduleHandler->invoke($name, 'theme', $args);

Future Plans

While currently there's only a Hook attribute, we expect eventually every core hook will have a subclass of Hook. This is where the hook doxygen will live instead of an api.php file and it'll also allow dropping the name from the attribute, for eg #[CommentInsert].

The base #[Hook] attribute will always work, the ability to fire and implement a hook without any additional code is considered a very important property of procedural hooks and we strive to maintain it.

This is just a plan and subject to change.

Follow-up issues converting hooks to OOP

This is section is updated dynamically, as new hooks are converted to OOP, compared to the "Hooks that have had support added in follow up issues" list, that captured the situation as of the date this change record was created.

Drupal 11.2

Drupal 11.3

Impacts: 
Module developers

Comments

mrweiner’s picture

FYI for anybody on 10.3.x: https://www.drupal.org/project/drupal/issues/3482464. At time of writing, these Hook classes do not exist. Should only affect phpstan, but there's an MR in there to add them to 10.3.

voleger’s picture

Is there any additional information about proper dependency injection practice for OOP hook implementation?

nicxvan’s picture

Discussion and questions are welcome here: #3493453: [meta] Standardize and clean up hook classes in core

macsim’s picture

...which doesn't need to add anything in the mymodule.services.yml and is even 8.x / 9.x / 10.0 compatible (if you don't use the LegacyHook attribute on those versions - Hook & LegacyHook came in core on 10.3.x version).

mymodule.module

use Drupal\Core\Hook\Attribute\LegacyHook;
use Drupal\node\Hook\NodeHooks;

#[LegacyHook]
function mymodule_user_cancel($edit, UserInterface $account, $method) {
  // This only needs to be returned if the hook previously had a return.
  return \Drupal::service('class_resolver')
    ->getInstanceFromDefinition(NodeHooks::class)
    ->userCancel($edit, $account, $method);
}

nicxvan’s picture

The moment services are injected this will break.

I would recommend removing this comment please.

macsim’s picture

The moment services are injected this will break.

Not sure about what you mean saying this. I am using this for ages (from Drupal 8 to 10 - it also works on 11 but I don't need it anymore) even with classes implementing dependencies injection.
If the class is a service then use the service directly, else use the class_resolver.
We're hereby talking about legacy code that we are going to drop soon or later anyway, so what's the point of declaring a service just to autowire the class when it doesn't need to be a service and will be autowired on d11 without that service declaration? To me it's just more code to drop

bbu23’s picture

I've also been using this approach for a long time, initially inspired from the content_moderation core module. I think it's better to provide arguments than asking someone to delete their comment, it's healthier and more productive.

murz’s picture

About hook_update_N() - there is a proposal to rework them using a totally new approach: #3167625: Deprecate/replace hook_update_N() in favor of an object-oriented approach similar to Laravel migrations

mxh’s picture

- deleted comment -

lostcarpark’s picture

PHPStan has been added to GitLabCI for previous major. This is causing both [Hook] and [LegacyHook] to be flagged as unknown attributes.

I tried like this:

/**
 * Comment about the hook.
 */
// @phpstan-ignore-next-line
#[LegacyHook]

But it resulted in a warning about an empty line.

What actually worked was:

/**
 * Amazing hook comment.
 *
 * @phpstan-ignore-next-line
 */
#[LegacyHook]

I needed to add this to both the .module file and the Hook class.

I have a concern that this will cause actual errors in the hook attributes to be missed, particularly when attributes for specific hooks are implemented in future.

lostcarpark’s picture

The need to add @phpstan-ignore-next-line so hook attributes won't trigger PHPStan warnings seems a bit ugly, and has potential to cause genuine errors to be missed.

If Drupal 10.6 were to define the attributes, even if they did nothing, it would prevent the PHPStan warning for previous major, and allow these lines to be removed.

I don't know enough about PHP attributes to know if this would be possible, but if PHPStan for previous major could warn if a [Hook] was implemented without a [LegacyHook] counterpart, that would be really helpful.

nicxvan’s picture

We now have parameters for Hook ordering. If we have a stub for the Hook attribute and there is a module that uses:
#[Hook('hook', order: OrderAfter)] and OrderAfter doesn't exist then there is a fatal error.

However if in addition Hook does not exist then nothing happens.

We could look into adding LegacyHook again since I can't see that getting parameters.

anybody’s picture

Here's the related change record: https://www.drupal.org/node/3493962

http://www.DROWL.de || Professionelle Drupal Lösungen aus Ostwestfalen-Lippe (OWL)
http://www.webks.de || webks: websolutions kept simple - Webbasierte Lösungen die einfach überzeugen!
http://www.drupal-theming.com || Individuelle Responsive Themes

mattlc’s picture

Thank you for the explanation of why Hook and LegacyHook classes have been removed.
Regarding that, don't you think we should update this page ?

  • Remove the "Backwards-compatible Hook implementation for Drupal versions from 10.1 to 11.0" paragraph
  • Add a statement that OOP Hooks can only be implemented from Drupal 11.0

OR

To allow contrib modules to be compatible with both Drupal 10 and 11

  • Strongly test that no issue is trigged on lower level (php) when a use statement uses an missing class (such as use Drupal\Core\Hook\Attribute\Hook; in Drupal 10) (I subscribe to @lostcarpark concerns)
  • Update the "Backwards-compatible Hook implementation for Drupal versions from 10.1 to 11.0" paragraph to add the @phpstan-ignore-next-line guideline.
anybody’s picture

Would be great to also document hooks that use placeholders, like hook_ENTITY_TYPE_presave.
I guess for "node" it will now be:

#[Hook('node_presave')]

But it's kind of special and should be documented.

http://www.DROWL.de || Professionelle Drupal Lösungen aus Ostwestfalen-Lippe (OWL)
http://www.webks.de || webks: websolutions kept simple - Webbasierte Lösungen die einfach überzeugen!
http://www.drupal-theming.com || Individuelle Responsive Themes

nicxvan’s picture

I'm not sure I'd consider that special case, it is:
#[Hook('ENTITY_TYPE_presave')] if your entity is node then it would be #[Hook('node_presave')] which is no different than procedural.
my_module_ENTITY_TYPE_presave would become my_module_node_presave

Does that help?

anybody’s picture

Yes, maybe my thoughts were just too complicated, I thought maybe others could also ask themselves if and how these replacements work for OOP hooks. So I think an example wouldn't hurt, but I'm also fine with not adding that.

http://www.DROWL.de || Professionelle Drupal Lösungen aus Ostwestfalen-Lippe (OWL)
http://www.webks.de || webks: websolutions kept simple - Webbasierte Lösungen die einfach überzeugen!
http://www.drupal-theming.com || Individuelle Responsive Themes

nicxvan’s picture

I just think we can document this in a better location. The change record is meant to inform about the new features not be an evolving source of documentation. L
Maybe here
https://www.drupal.org/docs/develop/creating-modules/understanding-hooks
Or here
https://api.drupal.org/api/drupal/core%21core.api.php/group/hooks/11.x

megakeegman’s picture

I am a bit confused by this line.

The LegacyHook attribute class is not required for this to work (PHP ignores it), but for certain tools it is still better to include it.

What exactly is the LegacyHook attribute class not required for? I read that paragraph a few times and I wasn't sure. Especially I just want to make sure that this does not contradict the statement a few sentences prior:

Versions of Drupal from 11.1 will not call node_user_cancel because it is marked with the #[LegacyHook] attribute; instead, the new implementation will be called directly.

which as I understand does a great job at explaining the function of the attribute.

For context (and hopefully to help clarify the above with an example), I am currently working on making this transition and working to support backwards compatibility. I have a legacy hook_module_implements_alter() implementation. My understanding is that this hook remains completely procedural and therefore should not be marked with the LegacyHook attribute, while any hooks being implemented in my hook class should.

nicxvan’s picture

The LegacyHook attribute is not required for the 10.1 bc layer to work. It is only required to prevent 11.1 from executing the hook twice.

hook_module_implements_alter has replacements in 11.2 see https://www.drupal.org/node/3496788 and the 3 or 4 related change records.
There is a LegacyModuleImplementsAlter to assist bc in the same way as you replace ordering with the parameters.

Be aware though that bc hmia can be very complex.

Feel free to reach out on slack in the contribute channel if you have any questions.