Easily transform hooks to an object-oriented implementation

When I started working with Drupal (7), hooks were one of the concepts I appreciated. Although not 100% conceptually correct, for me, this hooking system is just a simplified way to react to events. Allowing to make changes/add functionality, by creating a function and sticking to a naming convention. Drupal picks these up automatically and enables alterations without changing existing code. Brilliant!

However, everything comes with a price. When looking at a .module file of a mature custom module, its structure is typically chaotic. Dozens of hook implementations exist, having different parts of business logic mixed together. And all of these hooks are procedural code, making it way harder to write proper unit tests. Such code is hard to maintain, especially when having to add/change functionality after a couple of months by different developers.

Take a look at the .module files of Drupal's core Content Moderation and Workspaces modules. Most hook implementations use the 'class_resolver' service, and the business logic is moved to logically order classes. This structure brings order in the chaos and makes it easy to utilize the dependency injection pattern.

Take a look at the code example below, where the hook_entity_presave() is implemented:

...
/**
 * Implements hook_entity_presave().
 */
function example_entity_presave(EntityInterface $entity) {
  return \Drupal::service('class_resolver')
    ->getInstanceFromDefinition(EntityOperations::class)
    ->entityPresave($entity);
}
...

The example_entity_presave() function is called by Drupal every time an entity is about to be saved (i.e., inserted or updated). The 'class_resolver' service constructs our example EntityOperations object by invoking the create() function, ensuring a fictitious example manager service is available. Finally, the entityPresave() function is called, and the entity object is forwarded to this function. To keep the example simple, we let the example manager service perform a check, and based on that result, unpublish the entity; though, realistic cases would involve more complex logic that needs to be performed :)

<?php

declare(strict_types=1);

namespace Drupal\example;

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\example\ExampleManagernInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Defines a class for reacting to entity events.
 *
 * @internal
 */
class EntityOperations implements ContainerInjectionInterface {

  /**
   * The example manager service.
   *
   * @var \Drupal\example\ExampleManagernInterface
   */
  protected $exampleManager;

  /**
   * Constructs a new EntityOperations instance.
   *
   * @param \Drupal\example\ExampleManagernInterface $example_service
   *   The example manager service.
   */
  public function __construct(ExampleManagernInterface $example_service) {
    $this->exampleManager = $example_service;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('example.manager')
    );
  }

  /**
   * Acts on an entity before it is created or updated.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity being saved.
   *
   * @see hook_entity_presave()
   */
  public function entityPresave(EntityInterface $entity) {
    // As an example, use the example manager service to do a check and based on
    // that set the entity status unpublished.
    if ($this->exampleManager->performExampleCheck($entity)) {
      $entity->setUnpublished();
    }
  }

}

The above code example illustrates how easy it is to move the business logic of hooks away from .module files and incorporate a more object-oriented solution.

Category
Drupal
Hooks
Services
ClassResolver
Dependency Injection

Comments

Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.