slideshare quotation-marks triangle book file-text2 file-picture file-music file-play file-video location calendar search wrench cogs stats-dots hammer2 menu download2 question cross enter google-plus facebook instagram twitter medium linkedin drupal GitHub quotes-close
A DJ on his decks

Le système d'événements Drupal 9 permet aux différentes parties du système de communiquer de manière object-oriented. Au cours du traitement normal d'une page, Drupal déclenche des événements auxquels les composants adhèrent afin d'effectuer des actions. Le système d'événements est construit à partir du composant répartiteur d'événements Symfony, qui est une implémentation du pattern mediator design.

Le mediator design pattern est un moyen pour les objets de communiquer entre eux par le biais d'un objet médiateur (d'où son nom). L'idée est que les différentes parties du système d'événements ne sont jamais directement liées. Cela réduit les dépendances entre les objets et permet de découpler certaines parties de l'application. Les événements sont utiles dans une application vaste et modulaire comme Drupal, car ils permettent à différentes parties de l'application de travailler ensemble sans avoir à référencer explicitement les objets concernés.

Le système d'événements fonctionne en permettant aux différents services d'enregistrer leurs événements auprès du répartiteur d'événements. Ensuite, lorsqu'un événement est déclenché, le répartiteur d'événements invoquera les événements qui lui sont enregistrés, en transmettant des informations sur l'événement à la méthode invoquée.

Il existe une nombreuse variation de types d'événements dans Drupal, mais ils fonctionnent tous sur le même principe : souscrire à un événement et être déclenché au moment opportun. Pour savoir quels types d'événements sont utilisés dans Drupal, vous pouvez rechercher la chaîne "@Event" dans les commentaires. En recherchant cette chaîne, vous trouverez les événements par défaut du répartiteur d'événements de Symfony, les événements supplémentaires de Drupal Core et tous les autres événements définis par les modules contribués.

Vous pouvez également utiliser Drush et le web profiler fourni avec le module Devel pour connaître les événements qui existent ou qui ont été déclenchés pendant le chargement de la page.

Un bon exemple d'événement auquel on peut souscrire est "kernel.request". Cet événement est déclenché en premier, dès le début du chargement de la page, et constitue donc un bon moyen de faire des choses avant que d'autres composants n'aient interagi avec la page. Un bon exemple de cela est l'émission d'une redirection, qui est en fait l'événement auquel le module Redirect s'abonne afin d'exécuter cette fonction. Il peut le faire avant qu'un autre événement n'ait fait quoi que ce soit et permet de garder la redirection rapide.

Mise en œuvre d'un abonné aux événements

Pour créer un souscripteur d'événement pour surveiller un événement, il suffit d'ajouter un service et de l'étiqueter avec 'event_subscriber'. Prenons l'exemple de la redirection d'un utilisateur s'il tente d'accéder à une certaine page.

La première chose à faire est d'enregistrer notre classe de service comme un événement. Pour ce faire, nous créons un service de la manière habituelle, mais nous lui attribuons le nom de "event_subscriber". Nous ajoutons le code suivant à notre fichier my_module.services.yml dans le module appelé My Module.

services:
  my_module.message_exit:
    class: Drupal\my_module\EventSubscriber\PermissionRedirectSubscriber
    tags:
      - {name: event_subscriber}

Une bonne pratique consiste à ajouter la classe à un répertoire appelé EventSubscriber et à ajouter le mot "Subscriber" à la fin du nom de la classe. Cela donne une indication claire de ce que la classe est censée faire.

L'utilisation de services signifie que nous pouvons injecter dans le service toute autre dépendance dont nous pourrions avoir besoin. Comme nous voulons vérifier le rôle de l'utilisateur actuel, il est logique d'ajouter le service qui permet d'y accéder. 

services:
  my_module.message_exit:
    class: Drupal\my_module\EventSubscriber\PermissionRedirectSubscriber
    arguments: ['@current_user']
    tags:
      - {name: event_subscriber}

Nous sommes maintenant en mesure de commencer à créer la classe d'événements. Cette classe doit implémenter EventSubscriberInterface et cette interface nécessite la création de la méthode getSubscribedEvents(). Cette méthode doit retourner un tableau associatif d'événements auxquels l'abonné aux événements est abonné et les méthodes à appeler dans la classe pour chaque événement. Un poids facultatif peut également être appliqué au tableau pour ordonner le rappel par rapport aux autres abonnés du même événement. Le tableau associé de callbacks d'événements signifie que nous pouvons créer différentes réponses à différents événements dans la même classe si nécessaire.

Dans le code ci-dessous, nous configurons la classe avec la méthode getSubscribedEvents() et une méthode de rappel appelée checkRedirect(), qui ne fait rien pour le moment.

<?php

namespace Drupal\my_module\EventSubscriber;

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

class PermissionRedirectSubscriber implements EventSubscriberInterface {

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $account;

  /**
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The current user.
   */
  public function __construct(AccountInterface $account) {
    $this->account = $account;
  }

  public static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = ['checkRedirect'];
    return $events;
  }
  
  public function checkRedirect(GetResponseEvent $event) {
  }
}

La valeur de pondération de l'enregistrement ne devient importante que si nous voulons nous assurer que cet événement a été exécuté avant (ou après) un autre abonné d'événement dans le système. Le module de redirection a également un souscripteur d'événement qui effectue des redirections et qui a une pondération de 33. Dans ce cas, si nous voulions empêcher notre abonné d'interférer avec le module de redirection, nous pourrions définir la valeur à 34.

$events[KernelEvents::REQUEST][] = ['checkRedirect', 34];

Si vous vous abonnez à des événements, il est bon de jeter un coup d'œil aux autres souscripteurs du même événement. Cela vous permet de donner plus de poids à votre propre abonné afin d'éviter tout conflit.

Les composants du module étant en place, nous pouvons maintenant remplir la méthode checkRedirect. Tout ce que nous devons faire ici est de regarder le parcours actuel et si l'utilisateur n'a pas le rôle "administrateur", nous redirigeons la réponse. Pour ce faire, nous créons un objet RedirectResponse() et le définissons comme étant la réponse de l'événement (et par extension de la page).

<?php

namespace Drupal\my_module\EventSubscriber;

use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;

class PermissionRedirectSubscriber implements EventSubscriberInterface {

  public function checkRedirect(GetResponseEvent $event) {
    $routeName = RouteMatch::createFromRequest($event->getRequest())->getRouteName();

    if ($route_name == 'block.admin_display') {
      $roles = $this->account->getRoles();

      if (!in_array('administrator', $roles)) {
        $url = Url::fromRoute('entity.block_content.collection');
        $url = $url->toString();
        $response = new RedirectResponse($url);
        $event->setResponse($response);
        $event->stopPropagation();
      }
    }
  }

  // Rest of the class removed for brevity.
}

Configurer la réponse de l'événement à un nouvel objet RedirectResponse permettra de communiquer que la page devrait être redirigée. Nous invoquons également la méthode d'arrêt de la propagation de l'objet événement afin d'empêcher l'appel de tout autre événement. Ceci est utile si vous voulez vous assurer que rien d'autre ne peut interférer avec votre résultat.

L'objet événement transmis à la méthode subscriber contient toujours l'état actuel de la demande et de la réponse de la page. Cela vous permet d'extraire facilement de cet objet des informations telles que les parcours, les paramètres ou même les adresses IP. La partie réponse de l'objet événement correspond à l'état actuel de la demande. Dans l'exemple ci-dessus, nous la remplaçons par une redirection.

Créer vos propres événements

 

S'il est utile de surveiller les événements, il est également possible de définir vos propres événements afin de permettre aux abonnés de réagir à vos événements. Les événements que vous déclenchez dépendent entièrement de ce que votre module essaie de faire. Vous pouvez déclencher des événements à partir d'actions effectuées dans Drupal lui-même, mais vous pouvez également ajouter vos propres actions. Par exemple, si un utilisateur soumet un formulaire qui exécute une action, cette action peut déclencher un événement pour permettre aux modules de se brancher également sur l'action.

Plutôt que de mettre en place des événements personnalisés à titre d'exemple, j'ai décidé de montrer comment un module courant fait les choses à titre de démonstration. Le module Flag en est un parfait exemple, car il n'a pas beaucoup d'événements et il est facile de comprendre ce qui déclenche les événements (c'est-à-dire le marquage ou le dé-marquage du contenu).

Pour commencer, nous avons besoin d'une classe qui servira à nommer les événements. Il s'agit d'une classe de constantes en chaîne qui servent de noms aux événements. Dans le module Flag, deux événements sont définis. Un pour une entité qui est marquée, et un autre lorsque l'entité n'est pas marquée. 

<?php

namespace Drupal\flag\Event;

/**
 * Contains all events thrown in the Flag module.
 */
final class FlagEvents {

  /**
   * Event ID for when an entity is flagged.
   *
   * @Event
   *
   * @var string
   */
  const ENTITY_FLAGGED = 'flag.entity_flagged';

  /**
   * Event ID for when a previously flagged entity is unflagged.
   *
   * @Event
   *
   * @var string
   */
  const ENTITY_UNFLAGGED = 'flag.entity_unflagged';

}

En utilisant cette classe, si nous voulons nommer un des événements, nous pouvons simplement utiliser le code FlagEvents::ENTITY_FLAGGED pour un événement qui est déclenché par une entité qui est signalée.

Afin de transmettre des informations utiles à l'événement (dans l'esprit du modèle du médiateur), nous devons créer un objet que nous pouvons transmettre à l'événement. Tous les événements reçoivent un objet Event qui contient l'état de la demande et de la réponse. Le module Flag étend donc cette classe afin de fournir un contexte supplémentaire pour la fonctionnalité du module Flag.

<?php

namespace Drupal\flag\Event;

use Drupal\flag\FlaggingInterface;
use Symfony\Component\EventDispatcher\Event;

/**
 * Event for when a flagging is created.
 */
class FlaggingEvent extends Event {

  /**
   * The flagging in question.
   *
   * @var \Drupal\flag\FlaggingInterface
   */
  protected $flagging;

  /**
   * Builds a new FlaggingEvent.
   *
   * @param \Drupal\flag\FlaggingInterface $flagging
   *   The flaging.
   */
  public function __construct(FlaggingInterface $flagging) {
    $this->flagging = $flagging;
  }

  /**
   * Returns the flagging associated with the Event.
   *
   * @return \Drupal\flag\FlaggingInterface
   *   The flagging.
   */
  public function getFlagging() {
    return $this->flagging;
  }

}

Les classes FlagEvents et FlaggingEvent sont tout ce dont nous avons besoin pour commencer à déclencher des événements pour le module Flag. On peut le voir dans l'entité Flagging lorsqu'un nouveau drapeau est enregistré dans le système. Un événement est envoyé pour informer le système d'événements qu'une nouvelle entité a été marquée.

\Drupal::service('event_dispatcher')->dispatch(FlagEvents::ENTITY_FLAGGED, new FlaggingEvent($this));

Le module Flag en lui-même est à l'écoute de ses propres événements. On peut le voir dans la classe FlagCountManager, qui possède une méthode getSubscribedEvents() (et qui est également étiquetée comme un abonné aux événements dans le fichier de services du module).

  public static function getSubscribedEvents() {
    $events = [];
    $events[FlagEvents::ENTITY_FLAGGED][] = ['incrementFlagCounts', -100];
    $events[FlagEvents::ENTITY_UNFLAGGED][] = [
      'decrementFlagCounts',
      -100,
    ];
    return $events;
  }

La callback incrementFlagCounts() ci-dessus est responsable de l'écriture du drapeau dans la base de données.

Et les hooks?

Depuis longtemps, Drupal dispose d'un système appelé "hooks". Il s'agit de nommer les fonctions d'une certaine manière et Drupal les trouvera et les déclenchera lorsque certains événements se produiront. Par exemple, si vous implémentez le hook hook_page_attachments(), lorsque Drupal rendra le HTML d'une page, il appellera votre hook et ajoutera toutes les bibliothèques que vous avez ajoutées au chargement de la page.

Si vous êtes débutants Drupal, le système de hooks peut être un peu étrange, surtout si vous venez d'un monde object-oriented. Ils sont cependant très puissants et devraient faire partie de votre formation. Les hooks existent toujours dans Drupal 9 et sont toujours utilisés dans de nombreuses parties du système.

La seule différence réelle entre un hook et un événement est la manière dont vous informez Drupal de leur existence. Avec les hooks, vous nommez simplement votre fonction d'une certaine manière. Pour les événements, vous utilisez une classe et une certaine configuration.

À terme, le système d'événements et même de souscription remplacera le système de hooks dans Drupal. À un moment donné, au lieu de réagir à l'événement de connexion de l'utilisateur en utilisant hook_user_login(), vous créerez un abonné qui écoutera l'événement de connexion déclenché.

Je dois admettre qu'à l'heure actuelle, le fait d'avoir les deux systèmes intégrés dans le système est un peu déroutant. Il n'y a pas beaucoup de chevauchement entre les deux systèmes, car je n'ai pas été en mesure de trouver un crochet qui a également un événement déclenché. Vous devriez utiliser le système d'événements pour toute nouvelle fonctionnalité afin de vous préparer au mieux à ce changement.

Quand utiliser le système d'événements dans Drupal 9

Comme vous l'avez vu ci-dessus, créer des événements et s'y abonner est assez simple. Il y a quelques composants impliqués, mais une fois que vous comprenez ce qu'il faut rechercher, vous pouvez rapidement commencer à souscrire à des événements dans votre code personnalisé. Je limiterais le nombre d'événements de vos modules au minimum afin de garder les choses simples, mais vous devriez déclencher des événements dans votre code chaque fois que vous pensez que d'autres modules pourraient bénéficier de leur présence. Dans le module Flag, que j'ai examiné plus haut, les événements utilisés étaient minimes et faciles à comprendre et n'importe quel développeur pouvait écrire un événement pour s'abonner à cet événement et exécuter des fonctions personnalisées.

Comme le système d'événements de Drupal est object-oriented, cela signifie également que vous pouvez plus facilement tester la fonctionnalité des événements qu'avec les hooks. Vous pouvez même simuler l'objet événement qui est transmis à l'abonné, ce qui vous permet de tester l'abonné dans différentes situations.

Le système de hooks étant finalement remplacé par le système d'événements, vous devriez envisager de créer des événements et des souscrire par défaut. Une bonne façon d'anticiper les changements à venir est d'utiliser des services dans tous vos hooks par défaut. Cela signifie que lorsque vous devez migrer le code vers un nouvel événement, il vous suffit d'injecter votre service et d'exécuter le même code.

Démarrons ensemble votre projet Drupal


Contactez-nous