In the last lesson you met the simplest possible hook, hooks_example_help(). Now it's time for something meatier: hooks_example_node_view(), a hook implementation that maintains a live view counter using the session, and — as a bonus inside the very same function — defines and invokes a brand-new custom hook of its own. This is the same real source file as last lesson; you're now reading a different, richer part of it.
What you'll learn in this lesson
- What a dynamic hook is — one with an ALL-CAPS token in its name, like
hook_ENTITY_TYPE_view()— and why you'd choose it over the generic version - How to read and write to the current user's session from inside a hook implementation
- The three ways to invoke a hook from PHP:
invokeAll(),invoke(), andalter()— and when each one is the right tool - Why disabling the render cache with
#cache => ['max-age' => 0]is necessary for a counter like this one to actually count
The source file
Path (relative to the Examples module's root): modules/hooks_example/hooks_example.module — the same file from the previous lesson, reproduced in full again below so this lesson is self-contained; this time the explanation focuses on hooks_example_node_view().
<?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
Dynamic hooks: what the ALL-CAPS token means
function hooks_example_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
This implements hook_ENTITY_TYPE_view() — a dynamic hook, where part of the hook's name is a token, shown in ALL CAPS, that gets replaced with a real value. Replacing ENTITY_TYPE with node means this implementation fires only when a node entity is being rendered — not when a user, file, or taxonomy term is. The more generic hook_entity_view() exists too, and would fire for every entity type on the site; you'd reach for that one if you genuinely wanted to react to anything being viewed, regardless of type. Here, the module deliberately narrows its scope with the dynamic variant instead.
Tracking a per-session view count
$session = \Drupal::request()->getSession();
$current_counts = $session->get('hooks_example.view_counts', []);
if (!isset($current_counts[$entity->id()])) {
$current_counts[$entity->id()] = 1;
}
else {
$current_counts[$entity->id()]++;
}
$session->set('hooks_example.view_counts', $current_counts);
\Drupal::request()->getSession() retrieves the current visitor's session object — data that persists across page loads for that one visitor but isn't shared with anyone else. The counts are stored as an array keyed by node ID, defaulting to an empty array with get('hooks_example.view_counts', []) the very first time. Each visit either seeds the counter at 1 or increments the existing value, then writes the whole array back with set().
Invoking a custom hook: the three methods on module_handler
$module_handler = \Drupal::moduleHandler();
$module_handler->invokeAll('hooks_example_count_incremented', [$current_counts[$entity->id()], $entity]);
This is the flip side of everything you've learned so far: instead of implementing someone else's hook, this code defines and invokes its own. The module_handler service (an instance of ModuleHandlerInterface) offers three ways to do this:
| Method | Purpose |
|---|---|
invokeAll($hook, $args) | Calls every enabled module's implementation of the hook, aggregating any return values into an array. Used here. |
invoke($module, $hook, $args) | Calls only one named module's implementation — useful when you need a specific module's answer, not everyone's. |
alter($type, &$data, ...) | Passes data by reference so implementations can modify it in place — this is the mechanism behind every hook_form_alter()-style hook, covered in the next lesson. |
The string passed to invokeAll() — 'hooks_example_count_incremented' — deliberately omits the hook_ prefix; Drupal adds that back internally when scanning for implementations named {module}_hooks_example_count_incremented(). Custom hook names are conventionally prefixed with the defining module's own machine name, guaranteeing they can never collide with a hook name some other contributed module happens to invent.
Disabling the cache so the counter actually counts
$build['view_count'] = [
'#markup' => '<p>' . t('You have viewed this node @total times this session.', ...) . '</p>',
'#cache' => [
'max-age' => 0,
],
];
Drupal aggressively caches rendered output by default — which is exactly wrong for a live counter. '#cache' => ['max-age' => 0] tells the Render API "never serve a cached copy of this specific element," forcing this hook to run fresh on every single page load so the count stays accurate. Without this line, the very first render would get cached and every subsequent visitor (or the same visitor reloading) would see a frozen, stale number.
Quick check: if you removed
'#cache' => ['max-age' => 0]entirely, would the counter stop incrementing, or would it just stop displaying the correct incremented value? (The session logic itself would still run and increment correctly on the backend — but the rendered HTML could be served from Drupal's page cache instead of being freshly rendered, so a visitor might see an outdated number until the cache naturally expires or is cleared.)
See it for yourself
Create a node on your DDEV site (or use an existing one), then visit its page — you should see "You have viewed this node X times this session" appear beneath the content. Refresh the page and watch the number increase.
Two more reloads later: the counter reads 3, and the "first time you have viewed" message from the custom hook is gone entirely — it only ever fires once per session, exactly as the code you just read described. Log out and back in (or open a private window) and both reset, because the session itself is what's being tracked, not the node or your account.
Key takeaways
- Dynamic hooks (
hook_ENTITY_TYPE_view()) let you target one specific entity type by replacing the ALL-CAPS token — more explicit and efficient than checking the entity type yourself inside the generic variant. \Drupal::request()->getSession()is how you read and write per-visitor data that persists across page loads without touching the database.invokeAll(),invoke(), andalter()on themodule_handlerservice are the three ways to invoke hooks from PHP — call all implementations, call one specific module's implementation, or pass data by reference for modification.- A custom hook's name should be prefixed with your module's machine name to guarantee it can never collide with another module's hook of the same short name.
'#cache' => ['max-age' => 0]disables caching for one specific render element — essential for anything that needs to reflect state that changes on every request.
Coming up next
You've now implemented a standard hook and invoked a custom one. The next lesson looks at a completely different category: alter hooks — hooks that receive an already-assembled data structure and get to modify it before anyone else uses it. You'll see hooks_example_form_alter(), the third function in this same file, change the Drupal login form's field description without touching a single line of the login form's own code.