Defining and Invoking Your Own Custom Hooks in Drupalfor Drupal 11 , and 10

Last updated :  

You've spent this whole topic implementing hooks that Drupal core already defines. This final lesson closes the loop by looking at the other end of that relationship: a module that defines its own hook, invokes it, and — in the same file — also implements that same hook itself. If that sounds circular, that's exactly the point: there's nothing stopping the module that announces "something happened" from also being one of the modules that reacts to it.

What you'll learn in this lesson

  • How a module defines a brand-new hook name that didn't exist before
  • Why a module can implement its own custom hook, and why that's not a strange or unusual thing to do
  • The convention of documenting custom hooks in a separate {module_name}.api.php file
  • How %placeholder vs @placeholder differ inside t() strings

The source file

Path (relative to the Examples module's root): modules/hooks_example/hooks_example.module — the same file one last time; this lesson focuses on hooks_example_hooks_example_count_incremented(), working together with the invokeAll() call you first read two lessons ago.

<?php

/**
 * @file
 * Examples demonstrating how to implement and invoke hooks.
 */

use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\node\NodeInterface;
use Drupal\Core\Routing\RouteMatchInterface;

/**
 * @defgroup hooks_example Example: Hooks
 * @ingroup examples
 * @{
 * Demonstrates implementing, defining, and invoking hooks.
 *
 * Knowing how to implement, define, and invoke hooks is a critical concept for
 * any Drupal developer.
 *
 * Hooks are specially named functions called at key points in order to allow
 * other code to alter, extend, and enhance the behavior of Drupal core, or
 * another module. Without requiring changes to the original code.
 *
 * Every hook has three parts; a name, an implementation, and a definition.
 *
 * Hooks are implemented by following the function naming convention and
 * reviewing the documentation associated with a hook to discover parameters and
 * their expected values. Learn how to implement hooks by reviewing
 * hooks_example_help(), hooks_example_node_view(), and
 * hooks_example_form_alter() below.
 *
 * Because the list of hook implementations is cached you'll need to clear the
 * cache when first adding a new hook implementation.
 *
 * Hooks are defined by creating a new, unique, hook name, providing
 * documentation for the hook in an {MODULE_NAME}.api.php file, and using either
 * \Drupal\Core\Extension\ModuleHandlerInterface::invokeAll(),
 * \Drupal\Core\Extension\ModuleHandlerInterface::invoke(), or
 * \Drupal\Core\Extension\ModuleHandlerInterface::alter() via the
 * 'module_handler' service to call implementations of a hook in all enabled
 * modules. Learn how to define, and invoke a new hook by reviewing
 * hooks_example_node_view().
 *
 * Learn how to document a hook by reviewing hooks_example.api.php.
 *
 * @link https://www.drupal.org/docs/8/creating-custom-modules/understanding-hooks
 * Understanding hooks @endlink
 *
 * In order to see this example module in action you should create one or more
 * nodes on your site. Then visit those nodes and look for the view counter
 * added by this module. In addition, look for the special message displayed at
 * the top of a node the first time you view it.
 *
 * @see hooks
 * @see \Drupal\Core\Extension\ModuleHandlerInterface
 */

/**
 * Implements hook_help().
 *
 * When implementing a hook you should use the standard text "Implements
 * HOOK_NAME." as the docblock for the function. This is an indicator that
 * further documentation for the function parameters can be found in the
 * docblock for hook being implemented and reduces duplication.
 *
 * This function is an implementation of hook_help(). Following the naming
 * convention for hooks, the "hook_" in hook_help() has been replaced with the
 * short name of our module, "hooks_example_" resulting in a final function name
 * of hooks_example_help().
 */
function hooks_example_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    // For help overview pages we use the route help.page.$moduleName.
    case 'help.page.hooks_example':
      return '<p>' . t('This text is provided by the function <code>hooks_example_help()</code>, which is an implementation of <code>hook hook_help()</code>. To learn more about how this works checkout the code in <code>hooks_example.module</code>.') . '</p>';
  }
}

/**
 * Implements hook_ENTITY_TYPE_view().
 *
 * Some hook names include additional tokens that need to be replaced when
 * implementing the hook. These hooks are dynamic in that when they are being
 * invoked a portion of their name is replaced with a dynamic value. This is
 * indicated by placing the token words in all caps. This pattern is often used
 * in situations where you want to allow modules to generically act on all
 * instances of a thing, or to act on only a specific subset.
 *
 * There are lots of different entity types in Drupal. Node, user, file, etc.
 * Using hook_entity_view() a module can act on a any entity that is being
 * viewed, regardless of type. If we wanted to count views of all entities,
 * regardless of type this would be a good choice. This variant is also useful
 * if you want to provide administrators with a form where they can choose from
 * a list of entity types which ones they want to count views for. The logic in
 * the generic hook implementation could then take that into account and act on
 * only a select set of entity types.
 *
 * If however, you know you only ever want to act on viewing of a node entity
 * you can instead implement hook_ENTITY_TYPE_view(). Where ENTITY_TYPE is a
 * token that can be replaced with any valid entity type name.
 *
 * @see hook_entity_view()
 * @see hook_ENTITY_TYPE_view()
 */
function hooks_example_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
  // This example hook implementation keeps track of the number of times a user
  // has viewed a specific node during their current session. Then displays that
  // information for them when they view a node.
  //
  // In addition, a hook is invoked that allows other modules to react when the
  // page view count is updated.
  //
  // Retrieve the active session from the current request object.
  $session = \Drupal::request()->getSession();
  $current_counts = $session->get('hooks_example.view_counts', []);
  if (!isset($current_counts[$entity->id()])) {
    // If this is the first time they've viewed the page we need to start the
    // counter.
    $current_counts[$entity->id()] = 1;
  }
  else {
    // If they have already viewed this page just increment the existing
    // counter.
    $current_counts[$entity->id()]++;
  }

  // Save the updated values.
  $session->set('hooks_example.view_counts', $current_counts);

  // Invoke a hook to alert other modules that the count was updated.
  //
  // Hooks are invoked via the `module_handler` service. Which is an instance of
  // \Drupal\Core\Extension\ModuleHandlerInterface.
  //
  // Hooks can be invoked in a few different ways:
  // - All at once using ModuleHandlerInterface::invokeAll() to call all
  //   implementations of the specified hook provided by any enabled module.
  // - One at a time using ModuleHandlerInterface::invoke() to call only the
  //   the specified module's implementation of a hook.
  // - Using ModuleHandlerInterface::alter() to pass alterable variables to
  //   hook_TYPE_alter() implementations for all enabled modules. This method
  //   should be used for instances where the calling module has assembled data
  //   and would like to give other modules an opportunity to alter that data
  //   before it's used. A common pattern is to use invokeAll() to first gather
  //   input from other modules, the immediately afterwards call alter() to give
  //   modules the opportunity to alter the aggregate data.
  $module_handler = \Drupal::moduleHandler();

  // Calling \Drupal\Core\Extension\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 in your module it
  // is customary to prefix the hook name with the short name of your module
  // followed by the descriptive name of the hook itself. Because hooks names
  // are also PHP function names they should contain only lowercase alphanumeric
  // characters and underscores.
  //
  // The hook name parameter should have the "hook_" prefix removed. If you want
  // to invoke hook_user_login(), the value used here would be 'user_login'.
  //
  // Hook implementations can optionally return a value, depending on the hook
  // definition. If they do, the invokeAll() method aggregates the responses
  // from all hooks in an array and returns the array.
  //
  // In this example we're invoking hook_hooks_example_count_incremented() and
  // passing all implementations the current view count for the node, and the
  // node object itself.
  $module_handler->invokeAll('hooks_example_count_incremented', [$current_counts[$entity->id()], $entity]);

  // Display the current number of pages the user has viewed along with the
  // node's content.
  $build['view_count'] = [
    '#markup' => '<p>' . t('You have viewed this node @total times this session.', ['@total' => $current_counts[$entity->id()]]) . '</p>',
    // In order for this example to work we disable caching for the content of
    // this node completely. This ensures that our hook is called every time the
    // node is viewed instead of using a cached version of the page for
    // subsequent requests.
    '#cache' => [
      'max-age' => 0,
    ],
  ];
}

/**
 * Implements hook_form_alter().
 */
function hooks_example_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // This is an example of what is known as an alter hook. The $form parameter
  // in this case represents an already complete Form API array and our hook
  // implementation is being given the opportunity to make changes to the
  // existing data structure before it's used. Invoking an alter hooks is a
  // common pattern anytime lists or complex data structures are assembled.
  // hook_form_alter(), which allows you to manipulate any form, is one of the
  // most commonly implemented hooks.
  //
  // @see hook_form_alter()
  // @see hook_form_FORM_ID_alter()
  //
  // If this is the user login form, change the description text of the username
  // field.
  if ($form_id === 'user_login_form') {
    $form['name']['#description'] = t('This text has been altered by hooks_example_form_alter().');
  }
}

/**
 * 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()]));
  }
}

/**
 * @} End of "defgroup hooks_example".
 */

How it works

Where the custom hook is actually defined

Look back at hooks_example_node_view(), near the middle of the file. That function calls:

$module_handler->invokeAll('hooks_example_count_incremented', [$current_counts[$entity->id()], $entity]);

That single line is the entire act of defining and invoking a new custom hook, hook_hooks_example_count_incremented(). There's no separate "declare this hook exists" step in PHP — a hook is defined simply by the act of some code calling invokeAll() (or invoke() or alter()) with a new, never-before-used hook name. From that moment on, any module — including, as you're about to see, the very module that just invoked it — can implement {module_name}_hooks_example_count_incremented() and it will be found and called.

hooks_example_hooks_example_count_incremented() — implementing your own hook

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()]));
  }
}

Follow the naming convention from the very first lesson of this topic: take the hook name (hooks_example_count_incremented, as passed to invokeAll() — note it has no hook_ prefix to strip this time, since the module's own name already is the "prefix" in this hook's name) and prepend the implementing module's machine name. Because the implementing module here is the same hooks_example module that defined the hook, the function name ends up looking doubled: hooks_example_hooks_example_count_incremented(). That's not a typo — it's the naming convention working exactly as designed, just visually unusual because the defining module and the implementing module happen to be the same one.

The parameters match exactly what was passed into invokeAll(): the current view count, then the node entity. When the count equals 1 — meaning this is the visitor's first view of this node in their session — a status message announces it via \Drupal::messenger()->addMessage().

%title vs @total: two different placeholder behaviors in t()

Compare this lesson's t('...%title.', ['%title' => $node->label()]) with the earlier lesson's t('...@total times...', ['@total' => $current_counts[...]]). Both escape their values automatically for safe HTML output — but %-prefixed placeholders are additionally wrapped in emphasis (typically rendered italic) to visually distinguish user-supplied or dynamic content from the surrounding sentence, while @-prefixed placeholders are inserted as plain text. Drupal's translation system reserves a third style too, :-prefixed, for placeholders meant to be used as URLs. Choosing the right one is a small but genuinely useful convention to adopt in your own code.

Documenting a custom hook: the .api.php convention

A hook invented by invokeAll() inside a function body has no docblock of its own at the call site — so by convention, the full documented signature and contract for a custom hook lives in a separate, dedicated file: {module_name}.api.php (here, hooks_example.api.php). That file is never executed — it exists purely so other developers (and IDEs) can discover what arguments your custom hook passes and what, if anything, it expects back.

Quick check: could a completely different module — say, a hypothetical gamification module — also implement gamification_hooks_example_count_incremented() and have it run alongside this one? (Yes — that's the entire point of the pattern. invokeAll() calls every enabled module's implementation of the hook, not just the defining module's own. Any module, including ones that don't exist yet, can react the moment it implements a function with the right name.)

See it for yourself

Visit a node page on your DDEV site for the very first time in a fresh session (or after clearing cookies) — alongside the view counter from two lessons ago, you should also see a status message: "This is the first time you have viewed the node [title]." That message is produced entirely by the function you just read.

A node page showing the first-visit welcome message produced by the module's own custom hook implementation

Key takeaways

  • A custom hook is defined simply by calling invokeAll(), invoke(), or alter() with a hook name that didn't exist before — there's no separate declaration step.
  • The module that defines a hook is free to also implement it — you'll end up with a function name like {module}_{module}_{hook_name}(), which looks unusual but is the naming convention working correctly.
  • Any other enabled module can implement the same custom hook and will be called too, if the defining code used invokeAll() rather than invoke().
  • %placeholder in t() renders with emphasis for dynamic/user content; @placeholder renders as plain text; :placeholder is reserved for URLs.
  • Custom hooks should be documented in a dedicated {module_name}.api.php file, since there's no natural docblock location at the point where the hook is invoked.

Coming up next

You now understand hooks from every angle: implementing core's hooks, invoking hooks yourself, altering data by reference, and defining a hook others can implement. That's the foundation for the next topic, Blocks — where you'll meet your first real plugin type, and build a genuinely reusable, placeable piece of UI from scratch.