Loading...

Hooks vs Events in Drupal - Making an Educated Decision


Hooks vs Events in Drupal - Making an Educated Decision

Introduction

Drupal offers developers two primary methods, hooks and events, for extending its functionality. Understanding the differences between them and knowing when to utilize each can significantly impact the quality and performance of your Drupal projects.

Hooks

Drupal's foundational concept, hooks, enables modules to interact with and modify various aspects of Drupal's functionality. Hooks are predefined PHP functions that Drupal core or other modules call at specific points during a request's execution, allowing modules to alter data, modify forms, and react to system events.

Characteristics

  • Drupal's traditional approach to facilitating module interactions within the system.
  • Allows for independent and incremental enhancements or modifications to code.
  • Relies on hooks, which are predefined functions with specific names invoked by Drupal core or modules at different stages of execution.
  • Developers implement these functions within their modules to extend or modify default functionalities.
  • Known for its efficiency and straightforward implementation.

Drupal's hook system is integral to its extensibility and flexibility. Below is an example of how a hook is defined in a module:

// Implements hook_menu().
function mymodule_menu() {
  $items['mymodule/foo'] = array(
    'title' => 'My Module Foo Page',
    'page callback' => 'mymodule_foo_page',
    'access callback' => TRUE,
  );
  return $items;
}

function mymodule_foo_page() {
  return 'Hello from My Module!';
}
In this example, the `mymodule_menu()` function defines a new path (`mymodule/foo`) and associates it with a callback function (`mymodule_foo_page()`). When a user navigates to this path, the callback function is invoked, rendering the corresponding content. This demonstrates how developers can leverage hooks to extend Drupal's functionality and add custom features.

Categories of Hooks

  1. Event-triggered Hooks: These hooks respond to specific events, such as a user logging in, and trigger associated actions. They bear resemblance to Events and are invoked upon particular actions. For instance, hook_user_cancel().
  2. Information-providing Hooks: These hooks function akin to "info hooks," activated when a component requires details about a specific topic. These hooks typically return arrays with predefined structures and values determined by the hook definition. For example, refer to the user module's hook user_toolbar(), which appends links to the Toolbar on the common user account page.

    Note: In Drupal 8 and later versions, this functionality is generally managed by the plugin system, resulting in fewer conventional hooks compared to Drupal 7.

  3. Data Modification Hooks: Alteration hooks, typically suffixed with "alter," are invoked to enable modules to modify existing code. For example: hook_form_alter().

Illustration:

  • Drupal core hook: hook_form_alter()
  • Scenario: Adjusting a form defined by another module.
// my_module.module
function my_module_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  // Custom logic to modify the form. // Additional code to alter the form...
if ($form_id == 'my_form_id') {
  // Add or modify form elements.
  $form['new_element'] = [
    '#type' => 'textfield',
    '#title' => t('New Element'),
    // Additional properties...
  ];
}
}
}

Exploring Existing Hooks

Hooks can be defined by any contributed or custom modules. While some hooks are invoked by Drupal core subsystems, such as the Form API, others are always present. However, discovering available hooks and deciding which ones to implement can sometimes be challenging.

There are various methods to uncover existing hooks:

  1. Inspect hook definitions in *.api.php files within Drupal core or contributed modules.
  2. Utilize your IDE to search for functions beginning with hook_.
  3. Access a comprehensive list of hooks here.
  4. Alternatively, employ Drush commands to obtain a list of all implementations of a specific hook.
drush fn-hook help #Shortcut for drush devel:hook

Triggering a New Hook

  1. To enable other developers to modify or extend our feature, it's important to trigger a hook or dispatch an event.
  2. This can be initiated during any action, such as creating, deleting, updating, or when receiving or pushing data through an API.
  3. Hooks are triggered using the module handler service \Drupal::moduleHandler().
  4. Hooks can be triggered in various manners:
  • Execute the hook across all modules that implement it: \Drupal::moduleHandler()->invokeAll()
  • Trigger the hook per-module, typically by iterating over a list of enabled modules: \Drupal::moduleHandler()->invoke()
  • Invoke an alter hook, allowing for the modification of existing data structures using \Drupal::moduleHandler()->alter().

Defining a New Hook

To define a new hook, follow these steps:

  1. Choose a unique name for your hook.
  2. Document your hook.

Hooks are typically documented in a {MODULE_NAME}.api.php file:

// custom_hooks.api.php
/**
* Defines a custom hook for reacting to specific events.
*
* This hook is invoked when a certain event occurs in the system.
* Modules can implement this hook to perform additional actions in response to the event.
*
* @param string $param1
*    An example parameter for the hook.
* @param array $param2
*    Another example parameter for the hook.
*
* @ingroup custom_hooks_hooks
*/
function hook_custom_event($param1, array $param2) {
    // Your custom hook logic here.
}

Invoke your hook in your module's code:


/**
* Implements hook_ENTITY_TYPE_view().
*/
function hooks_example_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
    // Invoke a hook to alert other modules that the count was updated.
    $module_handler = \Drupal::moduleHandler();
    // In this example, we're invoking hook_custom_event().
    $module_handler->invokeAll('custom_event', [$entity]);
}

Choosing Between Events and Hooks

  • Events: Opt for events when you need to decouple actions or integrate with Symfony components.
  • Hooks: Use hooks for simpler modifications or when interacting with Drupal core and contributed modules.

Advantages and Disadvantages of Events vs. Hooks

Events:

  • Advantages: Decoupling, better organization, and integration with Symfony.
  • Disadvantages: Slightly higher learning curve, especially for those unfamiliar with Symfony.

Hooks:

  • Advantages: Simplicity, well-established in Drupal, and easier for Drupal-specific tasks.
  • Disadvantages: Tighter coupling, less organization in larger projects.

Events

Introduced in Drupal 8, events provide a more object-oriented approach to extending Drupal's functionality compared to hooks. In this event-driven architecture, event listeners or subscribers respond to specific events dispatched by other system components, executing custom logic when triggered.

Illustration

  • Core Drupal event: KernelEvents::REQUEST
  • Use case: Creating a bespoke module to intercept the REQUEST event for executing tailored operations prior to request processing.
// MyModuleEventSubscriber.php
namespace Drupal\my_module\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\RequestEvent;

class MyModuleEventSubscriber implements EventSubscriberInterface {

  public static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = ['onRequestEvent'];
    return $events;
  }
  public function onRequestEvent(RequestEvent $event) {
    // Custom logic to be executed on every request.
  }
}

Exploring Existing Events

There are various approaches to discovering existing events:

1. Utilizing the WebProfiler module:

  1. Download and activate the WebProfiler module, along with the Devel module since WebProfiler relies on Devel.
  2. Proceed to Manage > Configuration > Devel Settings > WebProfiler, then select the checkbox to enable the “Events” toolbar item.
  3. Upon visiting any page on your site, you'll notice the WebProfiler toolbar at the bottom. Clicking on the events toolbar icon will display a list of all event subscribers and associated information triggered during that request.

2. Utilize Devel to inspect an event class:

drush devel:event

 Enter the number for which you want to get information.:
  [0] kernel.controller
  [1] kernel.exception
  [2] kernel.request
  [3] kernel.response
  [4] kernel.terminate
  [5] kernel.view
 > 0

 Enter the number to view the implementation.:
  [0] Drupal\path_alias\EventSubscriber\PathAliasSubscriber::onKernelController
  [1] Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber::onController
  [2] Drupal\webprofiler\DataCollector\RequestDataCollector::onKernelController
 > 0

3. Look through your codebase for occurrences of @Event: 

Using your preferred editor like Visual Studio or PHPStorm, conduct a search for the text @Event with the file mask option set to: *.php.

Registering for an Event

Drupal operates on an event-driven architecture, enabling various components to interact by dispatching and registering for events.

Below is an example of registering for an event in Drupal 9/10.

1. Define an Event subscriber service 

# MyModule/my_module.services.yml
services:
  my_module.event_subscriber:
    class: Drupal\my_module\EventSubscriber\MyModuleEventSubscriber
    tags:
      - { name: event_subscriber }

2. Define an Event subscriber class

// MyModule/src/EventSubscriber/MyModuleEventSubscriber.php

namespace Drupal\my_module\EventSubscriber;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\Event;

/**
* Class MyModuleEventSubscriber.
*/
class MyModuleEventSubscriber implements EventSubscriberInterface {

  /**
  * {@inheritdoc}
  */
  public static function getSubscribedEvents() {
    // Specify the event(s) to subscribe to and the method to call when the event occurs.
    $events = [
      'node.insert' => 'onNodeInsert',
      'user.login' => 'onUserLogin',
    ];
    return $events;
  }

  /**
  * React to a node insert event.
  */
  public function onNodeInsert(Event $event) {
    // Your logic here.
    \Drupal::logger('my_module')->notice('Node inserted!');
  }
  /**
  * React to a user login event.
  */
  public function onUserLogin(Event $event) {
    // Your logic here.
    \Drupal::logger('my_module')->notice('User logged in!');
  }

  }

In this scenario:

  • The class MyModuleEventSubscriber implements the EventSubscriberInterface.
  • The getSubscribedEvents method specifies the events to which the subscriber is subscribed and the corresponding methods to invoke when each event occurs.
  • The onNodeInsert and onUserLogin methods contain the logic to execute when the respective events are triggered.

Triggering an Event

To enable another developer to subscribe to these events and react accordingly, you can trigger an event within your modules or submodules. Before triggering an event, it's important to determine when to do so. You might trigger an event to extend your module's functionality without modifying existing code. Events can be triggered at various points such as during data creation, updating, loading, or deletion managed by your module.

Let's illustrate this with an example.

Consider a scenario where we want other developers to take action when a new entity (specifically a node) is created after submitting a custom form.

1. Create a custom module (if don’t have):

# Create the module directory
mkdir modules/custom/custom_logger

# Create the module file
touch modules/custom/custom_logger/custom_logger.info.yml

2. Inside custom_logger.info.yml, add the following content:

```plaintext name: 'Custom Logger' type: module description: 'Custom module for logging events.' core_version_requirement: ^8 || ^9 || ^10 package: Custom ```

3. Establish an Event:

```plaintext // modules/custom/custom_logger/src/Event/CustomLoggerEvent.php namespace Drupal\custom_logger\Event; use Symfony\Component\EventDispatcher\Event; /** Defines the custom event for the custom_logger module. */ class CustomLoggerEvent extends Event { /** The node that triggered the event. @var \Drupal\node\Entity\Node / protected $node; /* CustomLoggerEvent constructor. @param \Drupal\node\Entity\Node $node The node that triggered the event. / public function __construct($node) { $this->node = $node; } /* Get the node object. @return \Drupal\node\Entity\Node The node. */ public function getNode() { return $this->node; } }

Comparing

Aspect Hooks Events
Flexibility Less flexible More flexible
Performance Potential performance impact due to execution of all hook implementations Generally better performance as only relevant listeners are triggered
Complexity May lead to complex code if not managed properly Requires understanding of Symfony's event dispatcher and object-oriented programming

Best Practices

  • Use hooks for simple modifications or alterations to Drupal's core functionality.
  • Use events for more complex and decoupled functionality that requires modularity and flexibility.
  • Adhere to Drupal coding standards and best practices to maintain code consistency and readability.

Conclusion

When deciding between hooks and events in Drupal development, consider the specific needs of your project. Both approaches offer advantages and disadvantages, and making an informed choice can lead to more maintainable and scalable Drupal applications.