Every hook you've used so far — hook_form_alter(), hook_entity_type_alter(), hook_user_login() — was defined by Drupal core. But the mechanism isn't core's alone to use. Any module can define a brand-new hook of its own, and the moment it does, every other enabled module gets the same ability to react to whatever that module is doing — with zero coupling between them. This lesson is where you flip from being a hook implementer to a hook definer.
What you'll learn in this lesson
- The naming convention for a hook your own module defines
- How to actually invoke a custom hook using the
module_handlerservice - A complete, real example: a hook that fires when a per-session view counter increments
A real example, start to finish
This is the exact pattern from Drupal's own Examples project. A module tracks how many times the current user has viewed a node this session, and defines a custom hook — hook_hooks_example_count_incremented() — that fires every time that count goes up:
// Inside hooks_example_node_view(), after incrementing the session counter:
$module_handler = \Drupal::moduleHandler();
// Calling ModuleHandlerInterface::invokeAll() will call implementations of
// the hook in question for all enabled modules. The method takes two
// arguments: the name of the hook to invoke, and an optional array of
// arguments to pass to any functions implementing the hook.
//
// Hook names need to be unique, so when defining a new hook it's customary
// to prefix the hook name with your module's short name followed by the
// descriptive name of the hook itself.
//
// The hook name parameter has the "hook_" prefix removed — to invoke
// hook_hooks_example_count_incremented(), the value used here is
// 'hooks_example_count_incremented'.
$module_handler->invokeAll('hooks_example_count_incremented', [
$current_counts[$entity->id()],
$entity,
]);
And here's an implementation of that brand-new hook — notably, written by the same module that defined it, which is completely normal; a module implementing its own hook is no different from any other module doing so:
/**
* Implements hook_hooks_example_count_incremented().
*
* Hooks can be implemented by both the module that invokes them, like we
* are doing here, as well as by any other enabled module.
*/
function hooks_example_hooks_example_count_incremented($current_count, NodeInterface $node) {
if ($current_count === 1) {
\Drupal::messenger()->addMessage(t('This is the first time you have viewed the node %title.', [
'%title' => $node->label(),
]));
}
}
Reading the naming convention carefully
Notice the implementation's function name: hooks_example_hooks_example_count_incremented. That's hooks_example (the implementing module) prefixed onto hooks_example_count_incremented (the hook name, which itself already starts with hooks_example because that's the module that defined it). The repetition looks odd at first glance but it's correct — the hook's own name already carries its defining module's prefix to guarantee it's globally unique, and every implementer's function name still needs its own module prefix on top of that.
invokeAll() vs. invoke() vs. alter()
invokeAll($hook, $args)— call every enabled module's implementation and collect their return values into an array. Use this for "let everyone react."invoke($module, $hook, $args)— call exactly one named module's implementation. Rare; used when you specifically need only one module's answer.alter($hook, &$data)— invoke every module'shook_HOOK_alter()implementation, passing$databy reference so each one can modify it in place. This is the mechanism behind every alter hook you've already met.
.api.php file. A hook you define is useless to other developers if nobody knows it exists. Drupal's convention is a file named your_module.api.php containing a documented, never-actually-invoked example implementation — purely so IDEs and other developers can discover the hook's name, parameters, and expected return shape.Quick check: you call
$module_handler->invokeAll('my_module_something_happened', [$data])but nothing happens, even though you've written an implementation. What's the single most common cause? (Answer: the cache. Just like implementations of core hooks, a newly-added implementation of your own custom hook won't be discovered until the cache is cleared.)
Key takeaways
- Defining a hook means picking a unique name (prefixed with your module's machine name) and invoking it yourself via the
module_handlerservice — there's no separate "declare a hook" step beyond that. invokeAll()calls every enabled module's implementation and collects return values;invoke()targets one module;alter()passes data by reference for in-place modification.- A module implementing its own custom hook is completely normal, not a special case.
- Document a hook you define in a
your_module.api.phpfile so other developers can discover it exists.
Coming up next
You've now covered the full arc of the hook system — implementing core hooks, controlling their order, altering forms/entity-types/menus, and defining your own. Next, we move into a different but related extensibility mechanism: Defining a Custom Service, where dependency injection replaces global function calls as the way modules share functionality.