Understanding Drupal Hooks: The Three-Part Pattern Explainedfor Drupal 11 , and 10

Last updated :  

Hooks are one of the most fundamental ideas in Drupal module development — and one of the most misunderstood by newcomers, because the word "hook" sounds abstract until you see one in action. By the end of this lesson you'll understand exactly what a hook is, why Drupal is built almost entirely out of them, and you'll have read a real, complete, working example module that implements four different hooks in four different styles.

Think of it like a suggestion box with a very specific inbox

Imagine a large office building where, at certain fixed moments — when a new employee starts, when a meeting room is booked, when someone submits an expense report — a specific, labeled inbox appears on a shared table. Any department that wants to react to "a new employee starts" can drop a note in that exact inbox, and it'll be read at exactly that moment, every time it happens, from then on. The person announcing "a new employee started" doesn't know or care which departments dropped notes in the inbox, how many did, or what they wrote — they just know the inbox existed at that moment and anyone who wanted to react, could.

That's a hook. Drupal core (and any module) declares "at this exact moment, in this exact place, here's an inbox — anyone can react." Your module reacts by writing a function with exactly the right name, and Drupal finds it and calls it automatically. No editing of core code, no central registry to update by hand, no coordination between the module announcing the moment and the modules reacting to it.

What you'll learn in this lesson

  • The three-part anatomy of every hook: a name, an implementation, and a definition
  • The naming convention that lets Drupal discover your hook implementation automatically, with zero registration code
  • What a "dynamic" hook is (one with an ALL-CAPS token in its name) and why it exists
  • A first, honest look at all four hook-related functions in this lesson's source file — you'll come back to this exact file three more times over the next three lessons, each time focused on a different function within it
Why this matters: once you understand hooks, huge swaths of Drupal core and contributed modules stop looking like magic. hook_form_alter(), hook_cron(), hook_entity_presave() — hundreds of these exist, and every single one follows exactly the pattern you're about to learn once.

The source file

Path (relative to the Examples module's root): modules/hooks_example/hooks_example.module

This one file contains four separate hook-related functions. This lesson introduces the whole file and focuses on the conceptual framing at the top (the @defgroup docblock) and the simplest function, hooks_example_help(). The next three lessons return to this exact same file, each time zooming in on one of the remaining three functions in real depth.

<?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

The three parts of every hook

The @defgroup docblock at the top of the file states the core idea plainly: every hook has three parts.

  • The name is the unique identifier — e.g. hook_help or hook_hooks_example_count_incremented.
  • The implementation is the function your module provides, following a strict naming convention (below).
  • The definition is the documentation — and, for a hook you invent yourself, the invocation code — that describes what the hook does, what arguments it receives, and what it's expected to return.

The naming convention: how Drupal finds your function without you registering it anywhere

When you implement a hook, you take its name and replace the hook_ prefix with your module's machine name. hook_help() becomes hooks_example_help() when implemented by the hooks_example module. That's the entire discovery mechanism: Drupal scans every enabled module for functions matching the pattern {module_name}_{hook_suffix}(). No config file lists which modules implement which hooks — the function name itself is the registration.

Common beginner mistake: the list of hook implementations is cached for performance. If you add a brand-new hook implementation to a module and it doesn't seem to run, the very first thing to try is clearing the cache — Drupal hasn't re-scanned for it yet.

hooks_example_help() — the simplest possible hook implementation

function hooks_example_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'help.page.hooks_example':
      return '<p>' . t('This text is provided by the function <code>hooks_example_help()</code>...') . '</p>';
  }
}

hook_help() is one of the most commonly implemented hooks in all of Drupal, and Drupal calls it whenever a help page is requested. The $route_name parameter identifies exactly which page is being loaded, which is why the function uses a switch statement — a module can (and often does) provide help text for several different routes from one function. The route help.page.hooks_example corresponds to this module's own help page, reachable at /admin/help/hooks_example.

Notice the docblock above the function just says "Implements hook_help()." — nothing more. That's a deliberate, standard convention: because full parameter documentation already lives on core's own hook_help() definition, repeating it here would just be duplication that inevitably goes stale. You'll see this exact one-line docblock pattern on every hook implementation in this file.

Quick check: if you renamed this module's machine name from hooks_example to my_module, what would the function need to be renamed to for Drupal to still find it as an implementation of hook_help()? (my_module_help() — the hook_ prefix is always replaced by the current module's own machine name, nothing else changes.)

See it for yourself

Visit /admin/help/hooks_example on your DDEV site — that's hooks_example_help(), running live, returning the exact HTML string from the code above.

The Hooks Example module's help page showing text returned by hook_help()

Key takeaways

  • Every hook has three parts: a name (the unique identifier), an implementation (your function, following the {module_name}_{hook_suffix}() naming convention), and a definition (the documentation describing the hook's contract).
  • The naming convention is the discovery mechanism — there's no separate registration step, no config file to edit.
  • Hook implementations are cached; always clear the cache the first time you add a new one to a module, or Drupal simply won't have found it yet.
  • The one-line docblock convention ("Implements hook_x().") signals that full parameter documentation lives on the hook's own definition, avoiding duplicated (and eventually stale) documentation.
  • hook_help() is called with the current route name, letting one function serve help text for multiple pages via a switch statement.

Coming up next

You've met the simplest hook in this file. The next lesson returns to this exact same hooks_example.module file and dives into hooks_example_node_view() — a more interesting hook that tracks a per-user view counter using the session, and along the way, defines and invokes a completely custom hook of its own.