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.
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.
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);
}
}
}
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.
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);
}
}
}
}
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.
The demo module for this article is in the X-Team Drupal 8 Examples Github repository.