Keep Moving Forward | X-Team Magazine

Drupal 8 Hooks Unravelled

Written by Deji Akala | Jul 21, 2017 4:00:00 AM

Drupal 8 is about Object-Oriented Programming (OOP) and a firm positioning of Drupal in the enterprise. The introduction of key Symfony components was meant to herald new beginnings and enforce a more professional programming paradigm.

The hook system which is as old as the project allows Drupal core to call certain functions defined in modules at specific places. In other words, Drupal allows developers to interact with core code when some things happen in the system, e.g. when a user logs in or out, a node is about to be saved, after it has been inserted in the database, updated, or deleted, etc.

Drupal 7 Implementation

When an event occurs and it is identified by a string which is the hook, e.g. user_login, node_insert, etc., Drupal calls any functions named according to the pattern of MODULE_HOOK with arguments to the function that have been specified by the module that introduces the hook. For a module called custom_hooks_demo, the function will be as follows:

/** * Redirect when a user has just logged in. * * @param $edit * The array of form values submitted by the user. * @param $account * The user object on which the operation was just performed. */
function custom_hooks_demo_user_login(&$edit, $account) {
	drupal_set_message(t('Hi %name! Welcome to our website', array('%name' => $account->name)));
	drupal_goto('<front>');
}

What is possible here, is only limited by the developer's imagination. You have all values submitted from the login form and the user object.

Some hooks are alterable by appending "alter" to the function names like with e.g. hook_TYPE_alter(), where TYPE may be form, links, image_styles, date_formats, system_info, node_grants, node_view, block_info, etc. These functions are handed over to drupal_alter(), in order to make sure that all alter operations are carried out consistently.

  function custom_hooks_demo_form_FORM_ID_alter(&$form, &$form_state, $form_id) {
  }	

Contributed modules are allowed to introduce their own hooks or alter hooks. For example, implementing Views hooks allows you to modify a lot of the functionality of this immensely popular module. Then, if I want to have a random display of views' results, I may implement hook_views_pre_render as follows:

/** * Scramble the order of the result rows displayed. * * @param $view * The view object about to be processed. */
function custom_hooks_demo_views_pre_render(&$view) {
	if ($view->name == 'random_nodes_view') {
		shuffle($view->result);
	}  
}

There are a couple of other views hooks that can be implemented to achieve the same result. This is a credit to the flexibility of both the views module and the hook system.

Finally, custom modules too can define their own hooks so that other developers can react to pre-determined events in the custom module.

Drupal 7 Invocation

A hook may be called in the following ways:

1 module_invoke_all($hook):

All enabled modules that implement the hook are discovered, and the hook functions called one after the other. $hook is the name of the hook.

module_invoke_all('node_presave', $node);

2 module_invoke($module, $hook):

A hook in a given module is called where $hook is the name of the hook and $module is the machine name of the module, e.g.:

$requirements = module_invoke('image', 'requirements', 'install');

3 In certain cases, the hook function has to be called directly, e.g. if arguments need to be passed by reference. Here is an example from the user module:

    // user.module
    function user_module_invoke($type, &$edit, $account, $category = NULL) {
	  foreach (module_implements('user_' . $type) as $module) {
	    $function = $module . '_user_' . $type;
	    $function($edit, $account, $category);
	  }
	}	

Then the function is called as follows:

	user_module_invoke('login', $edit, $user);
	user_module_invoke('presave', $edit, $account, $category);
	user_module_invoke('insert', $edit, $account, $category);
	user_module_invoke('update', $edit, $account);

Sometimes, you need to find all modules that implement a hook, before carrying out an action on each of them, e.g. to run all cron jobs.

<?php

function custom_hooks_demo_run_cron() {
  foreach (module_implements('cron') as $module) {
    try {
      module_invoke($module, 'cron');
      watchdog('custom_hooks_demo', 'Ran hook_cron from %name module.', array('%name' => $module));
    }
    catch (Exception $e) {
      watchdog_exception('cron', $e);
    }
  }
}

Drupal 8 Implementation

The EventDispatcher Component is the Symfony way of allowing components to interact with each other by registering, dispatching, and listening to events. This serves the same purpose as the hook system.

Drupal 8 does not only listen to Symfony-defined events, but it comes with its own events, which contributed and custom modules can listen for.

However, the hook system remains intact but with an OOP approach. Hooks available in Drupal 8 core may fall into the following categories:

1 Unchanged

Many hooks common in Drupal 7 are more or less the same, e.g. hook_user_login, hook_theme, hook_form_FORM_ID_alter, with some slight changes, mostly due to the architectural move to OOP. For example:

<?php
// D7
function custom_hooks_demo_user_login(&$edit, $account) {}
// D8
function custom_hooks_demo_user_login($account) {}
<?php

// D7
function custom_hooks_demo_help($path, $arg) {}
// D8
function custom_hooks_demo_help($route_name, RouteMatchInterface $route_match) {}

2 Removed

Some hooks have been completely removed, either because they are no longer required, or there are new ways of achieving the same result. For example hook_watchdog, hook_boot, hook_init, and hook_exit.

3 Modified

With the introduction of @EntityType annotation as part of the new Plugin system, hook_entity_info is no longer used to define entity types. However, this hook was replaced with hook_entity_type_build, to add information to existing entity types. Entity definitions can be changed with hook_entity_type_alter.

4 Added

Several new hooks have been added, particularly related to entities. In Drupal 7, there are 12 hook_entity_* hooks, while in Drupal 8, there are 43 hook_entity_* and 19 hook_ENTITY_TYPE_* ones.

Drupal 8 Invocation

This is where there is a divergence from Drupal 7. The core/lib/Drupal/Core/Extension/ModuleHandler.php class manages modules and it is provided as the module_handler service.

There are two ways of getting a service.

a. To accommodate legacy procedural code, there is a service container wrapper class core\lib\Drupal.php, which can be called statically. The moduleHandler() method returns the module_handler service.

$moduleHandler = \Drupal::moduleHandler();

Then you can do:

$moduleHandler->invokeAll('node_presave');
// or
$requirements = $moduleHandler->invoke('image', 'requirements', ['install']);

These calls are also chainable, so you can write the above as:

\Drupal::moduleHandler()->invokeAll('node_presave');
// or
$requirements = \Drupal::moduleHandler()->invoke('image', 'requirements', ['install']);

Let us implement the same hook_user_login example above in Drupal 8.

function custom_hooks_demo_user_login($account) {
  drupal_set_message(t('Hi %name! Welcome to our website', ['%name' => $account->getDisplayName()]));
  return new RedirectResponse(\Drupal::url('<front>', [], ['absolute' => TRUE]));
}

Before this can work, you need to import the RedirectResponse class right at the top of the file, just after the opening PHP tag, e.g.

<?php

use Symfony\Component\HttpFoundation\RedirectResponse; 

This is how to invoke all implementations of a hook in a module file, e.g. to run all cron jobs, just like in Drupal 7 above.

<?php

function custom_hooks_demo_run_cron() {
  foreach (\Drupal::moduleHandler()->getImplementations('cron') as $module) {
    try {
      \Drupal::moduleHandler()->invoke($module, 'cron'); 
      \Drupal::logger('')->notice('Ran hook_cron from @name module.', array('@name' => $module));
    }
    catch (\Exception $e) {
      watchdog_exception('cron', $e);
    }
  }
}

b. The recommended way in classes is through dependency injection. Your class needs to implement the ContainerInjectionInterface and you inject the ModuleHandlerInterface into your class constructor. Then you get an instance of ModuleHandler from the module_handler service in the service container. This will allow you to do something like the following:

$this->moduleHandler->invokeAll('node_presave');
// or
$requirements = $this->moduleHandler->invoke('image', 'requirements', ['install']);

Here is an example of a Controller class where we inject ModuleHandlerInterface and LoggerInterface through the constructor method. In the customRunAllCrons() method, we get all modules that implement hook_cron, run, and log each one.

<?php
// src/Controller/CustomHooksDemoController.php

namespace Drupal\custom_hooks_demo\Controller;

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleHandler;
use Psr\Log\LoggerInterface;

class CustomHooksDemoController implements ContainerInjectionInterface {

  /** * The module handler. * * @var \Drupal\Core\Extension\ModuleHandlerInterface */
  protected $moduleHandler;

  /** * A logger instance. * * @var \Psr\Log\LoggerInterface */
  protected $logger;

  /** * Constructs a new ModuleHandler object. * * @param \Drupal\Core\Extension\ModuleHandler $module_handler * The module handler. * @param \Psr\Log\LoggerInterface $logger * A logger instance */
  public function __construct( ModuleHandlerInterface $module_handler, LoggerInterface $logger ) {
    $this->moduleHandler = $module_handler;
    $this->logger = $logger;
  }

  /** * @inheritdoc */
  public static function create(ContainerInterface $container) {
    return new static (
      $container->get('module_handler'),
      $container->get('logger.factory')->get('custom_hooks_demo')
    );
  }

  /** * Run all hook_cron implementations and log each run. */
  public function customRunAllCrons() {
    foreach ($this->moduleHandler->getImplementations('cron') as $module) {
      try {
        $this->moduleHandler->invoke($module, 'cron');
        $this->logger->notice('Ran hook_cron from @name module.', array('@name' => $module));
      } catch (\Exception $e) {
        watchdog_exception('cron', $e);
      }
    }
  }
}

Conclusion

The hook system remains largely unchanged in Drupal 8. However, you need to be aware of any changes to particular hooks, e.g. the number and order of arguments required. Make sure to check on those that have been either completely removed or just renamed. Above all, check out the new ones, especially for interacting with entities and entity types.

Ideally, if you identify any events you can subscribe to, the EventDispatcher system is the way forward when interacting with other components of your application.

Code

The demo module for this article is in the X-Team Drupal 8 Examples Github repository.

Recommended reading

  1. Hooks
  2. entity.api.php
  3. Change records for Drupal core