Hux - mejor gestión de hooks en Drupal

gancho
Solucionex
24
Nov 23

TL;DR: Hux es un módulo que permite implementar los hooks de Drupal de manera orientada a objetos de forma sencilla.

Los hooks de Drupal, esas funciones que permiten modificar el comportamiento de Drupal al gusto, y que a la mínima que se tenga un proyecto grande se puede terminar con archivos .module gigantescos... y a finales de 2023 todavía no hay una intención clara de cambiarlos por algo mejor.

Aunque existen maneras de abstraer la funcionalidad de los hooks tanto en el propio core (llamando a métodos de clases con ClassResolver) como en módulos contribuidos (por ejemplo Hook Event Dispatcher, usando eventos en lugar de hooks), existe el módulo Hux que permite directamente convertir métodos normales de clases en hooks.

Funcionamiento de Hux

El procedimiento para implementar hooks con Hux es el siguiente:

  1. Crear un método en una clase dentro de la carpeta Hooks de un módulo custom
  2. Crear una función que reciba los mismos argumentos que el hook
  3. Implementar el atributo #Hook([nombre del hook]) / #Alter([nombre del hook_alter])
  4. Limpiar caché, en caso de que la clase sea nueva

Nota: Hux es complementario al sistema de hooks de Drupal, lo que significa que los hooks normales que ya existiesen o los nuevos que se creen de la manera normal, seguirán funcionando.

Detalle de las implementaciones

Hooks normales

Ejemplo con hook_views_pre_build:

<?php

declare(strict_types = 1);

namespace Drupal\mymodule\Hooks;
use Drupal\hux\Attribute\Hook;

/**
 * OOP version of views hooks.
 */
final class ViewsHooks {

  /**
   * Do something.
   */
  #[Hook('views_pre_build')]
  public function descriptiveFunctionName(ViewExecutable $view): void {
    // Code here...
  }
  
}

Importante: El nombre del hook hay que ponerlo sin el prefijo "hook_".

Hooks de alteración

Ejemplo con hook_form_alter:

<?php

declare(strict_types = 1);

namespace Drupal\mymodule\Hooks;

use Drupal\Core\Form\FormStateInterface;
use Drupal\hux\Attribute\Alter;

/**
 * OOP version of form hooks.
 */
final class FormHooks {

  /**
   * Do something.
   */
  #[Alter('form')]
  public function descriptiveFunctionName(array &$form, FormStateInterface $form_state, string $form_id): void {
    // Code here...
  }
  
}

Importante: El nombre del hook hay que ponerlo sin el prefijo "hook_" ni el sufijo "_alter".

Reemplazar un hook existente de otro módulo

Ejemplo con hook_user_login del módulo user:

<?php

declare(strict_types = 1);

namespace Drupal\mymodule\Hooks;

use Drupal\Core\Form\FormStateInterface;
use Drupal\hux\Attribute\ReplaceOriginalHook;

/**
 * OOP version of user hooks.
 */
final class UserHooks {

  /**
   * Do something.
   */
  #[ReplaceOriginalHook(hook: 'user_login', moduleName: 'user')]
  public function userFunction(AccountInterface $account) {
    // Code here...
  }
  
}

Importante: El nombre del hook hay que ponerlo sin el prefijo "hook_".

Peso / prioridad

Para especificar la prioridad en caso de que se tengan varias implementaciones del mismo hook se usa el atributo "priority":

#[Hook('entity_access', priority: 100)]
public function entityAccess1(...) {
}

#[Hook('entity_access', priority: 200)]
public function entityAccess2(...) {
}

Implementar varios hooks con la misma función

Simplemente hay que implementar un array de atributos Hook:

#[
  Hook('entity_insert'),
  Hook('entity_update'),
  Hook('entity_delete'),
]
public function myEntityAccess(EntityInterface $entity): void {
}

Inyección de dependencias

Al usarse clases normales, puede usarse la inyección de dependencias haciendo que la clase implemente la interfaz ContainerInjectionInterface:

<?php

declare(strict_types = 1);

namespace Drupal\mymodule\Hooks;

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\hux\Attribute\Hook;
use Drupal\views\ViewExecutable;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * OOP version of views hooks.
 */
final class ViewsHooks implements ContainerInjectionInterface {

  /**
   * Class constructor.
   */
  public function __construct(
    private EntityTypeManagerInterface $entityTypeManager,
  ) {
  }
  
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('entity_type.manager'),
    );
  }
  
  /**
   * Do something.
   */
  #[Hook('views_pre_build')]
  public function descriptiveFunctionName(ViewExecutable $view): void {
    // Code here...
    $this->entityTypeManager->getStorage('node')->load(...);
  }
  
}

Modo rendimiento

Hux permite una ligera mejora de rendimiento al cachear las definiciones de hooks. En producción, lo ideal es añadar lo siguiente al archivo services.yml:

parameters:
  hux:
    optimize: true

Más info.

Limitaciones

Por el momento no es posible implementar los hooks_preprocess usando Hux.

Imagen: Grace To en Unsplash

Drupal
Herramienta desarrolladores