Drupal 8 - Le système de hook

Le bon vieux système de hook

Nous arrivons bientôt au terme de la série d'article concernant la Création d'un module avec Drupal 8. Nous avons pu experimenter beaucoup de nouveaux concepts avec l'orientation POO de Drupal 8: les entités, les formulaires, les éléments de formulaire (@FormElement), les types et widget de champs, les services. Nous avons implémenté beaucoup de classes pour arriver à notre but. Reste maintenant à finaliser ce travail pour que notre module soit fonctionnel.

Oui, nous avons toute une série d'éléments à notre disposition, mais en l'état, notre module ne fait toujours pas ce qu'on lui demande de faire, c'est à dire remplacer des mots/phrases par des liens. C'est maintenant grace au système de hook bien connu de Drupal que nous allons pouvoir assembler les dernières pièces et rendre notre module pleinement fonctionnel.

Analyse

Comme je l'avais expliqué au départ, le module que nous sommes en train de mettre en place, je l'ai déjà créé avec Drupal 7. Il s'était inspiré d'une série de modules orientés SEO. Tous ces modules ne correspondaient malheureuement pas aux besoins. Les uns manquaient de fonctionnalités, les autres ne prenaient pas en charge le multilingue ou d'autres aspects nécessaires. j'avais donc entrepris de développer mon propre module en m'inspirant des fonctionnalités intéressantes des modules existants.

Un des points sur lequel je ne pouvais faire l'impasse concernait la manière d'appliquer le parsing. Je voulais pouvoir sélectionner les champs sur lesquels le parsing serait exécuté, et je voulais également que ce parsing ne s'effectue pas à chaque affichage d'un contenu. La solution avait été d'utiliser hook_field_attach_load. Dans Drupal 7, ce hook s'exécute sur tous les champs d'une entité avant d'effectuer le rendu. L'intérêt de passer par ce hook, c'est que le rendu est mis en cache dans la table cache_field. Ce hook permet donc d'effectuer des modifications avant que le rendu HTML ne soit finalisé et mis en cache. Nous avons donc un seul parsing effectué pour un contenu particulier tant que le cache n'est pas invalidé. Ce hook correspondait donc bien à mes besoins pour Drupal 7.

Avec Drupal 8, le système de cache a été entièrement revu, proposant plusieurs fonctionnalités très pratiques tels que les tags, les contextes ou encore le concept de "max-age".

Avant de nous concentrer sur les caches, le premier problème était de trouver la bonne manière d'appliquer le parsing. J'ai tout d'abord pensé à une sorte de "Decorator" qui viendrait se greffer sur les classes du type EntityViewBuilder, afin d'accéder à la méthode de rendu d'une entité pour la surdéfinir, mais plusieurs tests dans ce sens se sont avérés infructueux. Remplacer, par un moyen ou un autre, chaque classe ViewBuilder pour chaque entité me semblait une très mauvaise approche. Chacune de ces classes ayant ses propres spécificités, ça semblait fastidieux et difficile à maintenir. C'est pour ça que l'idée du décorateur avait l'air intéressante.

Pour ne rien vous cacher, Drupal 8 étant orienté POO, je voulais vraiment trouver une solution orientée POO et m'affranchir du système de hook pour avoir des solutions plus élégantes, mais malheureusement, je n'y suis pas arrivé.

Et le gagnant est...

hook_entity_view, tout simplement ! Ce dernier s'exécute lors de la visualisation d'une entité, possède une série d'arguments dont le view_mode qui indique le mode de visualisation en cours (teaser, full, print, etc...). De plus, Drupal 8 a intégré un système de cache de "rendu". Par défaut, le rendu d'une entité est automatiquement mis en cache, et il est possible d'utiliser les "cache tags" pour indiquer à quel moment le cache de rendu doit être invalidé et reconstruit. Au final, hook_entity_view possède tout pour plaire, ou presque. Le seul défaut que je peux lui reprocher consiste dans le fait que dans mon implémentation, j'applique les "cache tags" sans savoir si le rendu doit réellement être invalidé, mais nous verrons ça dans le prochain article.

Mise en place du parseur

Au final, le contrôle indiquant si notre entité et élligible pour internal_link est intégré dans hook_entity_view, et le parsing en lui-même est exécuté via une méthode de type #post_render appliquée sur les champs sélectionnés dans la configuration globale.

Dans le fichier .module du module internal_link, j'ai donc rajouté l'implémentation de hook_entity_view ci-dessous:

/**
 * Implements hook_entity_view.
 *
 * @param array $build
 *   The build array.
 * @param EntityInterface $entity
 *   The current entity to display.
 * @param EntityViewDisplayInterface $display
 *   The current display configuration.
 * @param string $view_mode
 *   The current view mode.
 */
function internal_link_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
 
  if ($entity instanceof ContentEntityInterface && $view_mode == 'full') {
 
    // Retrieve internal link parser and check if
    // current entity must be parsed.
 
    /** @var \Drupal\internal_link\ParserService $parser */
    $parser = \Drupal::service('internal_link.parser');
    $info = $parser->mustBeParsed($entity);
 
    if ($info !== FALSE) {
 
      $fields = $info->getSettings()->getFields();
 
      foreach ($fields as $field_name => $field) {
        if (isset($build[$field])) {
 
          // If we found the field to be parsed,
          // we store in the internal link informations
          // and add a post render process.
          $build[$field]['#internal_link'] = $info;
          $build[$field]['#post_render'][] = 'internal_link_post_render';
        }
      }
    }
  }
}

Rien de très compliqué. Nous testons d'abord si l'entité passée en argument est une entité de type contenu, et si le mode d'affichage correspond au mode complet (full).

Nous récupérons ensuite le service de parsing que nous avons implémenté auparavant, et nous appelons sa méthode mustBeParsed en passant l'entité en cours d'affichage en argument. Cette méthode va effectuer les tests nécessaires pour savoir si l'entité est élligible. Si le résultat retourné correspond à un objet de type InternalLinkParserInfo alors nous pouvons continuer le processus.

Nous récupérons la liste des champs sur lesquels le parsing doit être effectué et pour chaque champ, nous allons y intégrer une clé #internal_link dont le contenu correspond aux infos de parsing. Nous ajoutons une fonction de post rendu via la clé #post_render. Cette fonction est exécutée après la construction du résultat de rendu. Elle propose donc d'effectuer des modifications sur le rendu HTML final avant qu'il soit affiché.

Voici maintenant la fameuse fonction de post rendu:

/**
 * Post render method for parsed elements.
 *
 * @param string $markup
 *   The text in element.
 * @param array $element
 *   The parsed element as rendered array.
 *
 * @return string
 *   The parsed text.
 */
function internal_link_post_render($markup, $element) {
 
  /** @var \Drupal\internal_link\ParserService $parser */
  $parser = \Drupal::service('internal_link.parser');
  $markup = $parser->parse($markup, $element['#internal_link']);
  return $markup;
}

L'argument $markup correspond au rendu HTML final, l'argument $element correspond à l'élément sur lequel le callback #post_render a été ajouté.

L'implémentation est des plus simples. Nous récupérons à nouveau notre service de parsing et appelons sa méthode parse en lui passant le rendu final ainsi que les informations de parsing. Une fois la méthode exécutée, elle retourne le rendu final modifié si nécessaire, que nous retournons en sortie de fonction.

Conclusion

La boucle est presque bouclée. Via hook_entity_view, nous avons pu tester l'élligibilité de l'entité en cours de visualisation afin d'appliquer le parsing du texte lorsque c'est nécessaire. Ce parsing s'exécute sur le rendu HTML final, ainsi il est appliqué juste avant l'affichage de l'entité.

Nous pouvons configurer de manière globale pour activer les liens internes sur un type d'entité (par exemple contenu) et appliquer les liens interne sur un ou plusieurs bundle (par exemple article). Nous pouvons ensuite créer un ou plusieurs liens internes, puis un contenu article contenant dans le texte les mots correspondant aux liens internes. Nous verrons que le processus est fonctionnel.

Néanmoins, il reste un point important à finaliser: l'invalidation du cache de rendu. Car même si, lors d'un ajout ou d'une édition d'article, le parsing est effectué, il est mis en cache. Si nous ajoutons par la suite un nouveau lien interne, ce dernier ne sera pas pris en compte dans le système tant que nous n'aurons pas vidé les caches. Nous verrons dans l'article suivant comment automatiser l'invalidation du cache pour ne plus avoir à se soucier de ce problème.

Ajouter un commentaire

CAPTCHA
Cette question permet de savoir si vous êtes un visiteur ou un robot de soumission de spams