Drupal 8 - Performances

La mise en cache avec Drupal 8

Dernière ligne droite pour les articles sur la Création d'un module avec Drupal 8. Après avoir vu l'implémentation de hook_entity_view dans l'article précédent, nous allons maintenant nous pencher sur la mise en cache des données.

Problématique

Comme expliqué dans l'article précédent, modifier le rendu HTML d'une entité pose un problème de taille. Le rendu étant mis en cache avec le nouveau système intégré à Drupal 8, l'ajout (ou la modification) d'un lien interne ne sera pris en considération que lorsque les caches seront vidés. Au mieux si on modifie le contenu succeptible de contenir le mot à remplacer par un lien, au pire lorsque le webmaster demande une invalidation totale des caches. L'invalidation peut s'avérer très aléatoire, et c'est pour cette raison qu'un travail de compréhension du système de cache était nécessaire.

Le cache de rendu

Le système de cache de Drupal 8 a été longuement réfléchi. Drupal ayant toujours été relativement gourmand en terme de ressources, il était nécessaire de palier les lenteurs du système en élaborant un système efficace qui puisse être utilisé à différents niveaux. Un des niveaux de réflexion correspond au cache de rendu. On pourrait l'apparenter au "cache_field" de Drupal 7, bien qu'il soit plus élaboré et basé sur une série de techniques globales intéressantes au niveau de l'invalidation.

Avec Drupal 7, la table "cache_field" stocke le rendu des champs d'une entité afin d'éviter de trop nombreuses exécutions de processus. Avec Drupal 8, le cache de rendu va bien plus loin. Il permet de stocker tous les rendu d'entités en regard du mode de visualisation, du langage, des permissions utilisateurs... En gros, il est très flexible. Si nous ajoutons le concept de "max-age" et celui des tags d'invalidation, nous avons un système puissant qui va amener une invalidation fine des contenus en cache.

Imaginons par exemple la chose suivante:

Un contenu de type article est affiché dans différentes structures de données:

  • Dans des blocs affichés sur des pages du site
  • Dans des listings d'articles
  • En mode visualisation complète

Chaque structure de données est mise en cache pour améliorer les performances du site web. Chaque rendu est "tagué" avec une ou plusieurs informations, généralement des paires de clés/valeurs. Ces informations vont définir les processus d'invalidation.

Mon article possède l'identifiant 1 (node:1). Cette paire va être intégrée partout ou l'article apparait. Si cette paire nécessite une invalidation, elle est passée à l'API de cache, et Drupal va rechercher toutes les entrées de cache possédant ce tag et les supprimer.

Concrètement, lorsque l'auteur modifie l'article en question, la paire node:1 est demandée pour l'invalidation. chaque structure de données qui possède ce tag va être supprimée de la table cache_render pour que le rendu soit reconstruit au prochain affichage.

Ainsi, nous avons un système efficace qui s'occupe d'invalider les caches uniquement lorsque ça s'avère nécessaire. Nul besoin de vider tous les caches, le système gère une bonne partie de ce processus en interne.

Vient ensuite la personnalisation. Il est tout à fait possible d'indiquer a des tableaux de rendu des tags spécifiques pour effectuer ses propres invalidations. C'est ce que nous allons voir maintenant.

Analyse

Deux étapes vont être nécessaires à la mise en place de notre personnalisation du cache.

La première va être d'ajouter les tags nécessaires aux tableaux de rendu qui passent par la méthode de parsing. La seconde sera d'implémenter la méthode getCacheTagsToInvalidate de notre type d'entité internal_link.

Le seul grief que je peux avoir contre ma propre solution est le suivant:

Nous utilisons une méthode de #post_render afin d'effectuer le parsing du texte déjà "rendu". Si je passais par une méthode de #pre_render, il pourrait s'avérer très complexe de retrouver la valeur à parser, dû au fait que chaque champ peut avoir une structure de données différente. C'est pour cette raison que la méthode #post_render est pratique. Elle prend en argument une variable nommée $markup, dont le rendu HTML a déjà été effectué, mais pas encore mis en cache.

Le problème, c'est qu'à ce niveau du processus, nous ne sommes plus en mesure d'attribuer de nouveaux tags de cache. La méthode doRender de la classe Renderer.php (/core/lib/Drupal/Core/Render/Renderer.php) s'exécute d'une certaine manière. Et si il est possible d'attribuer des tags de cache au niveau #pre_render, ce n'est plus possible au niveau #post_render. C'est d'ailleurs directement après l'exécution des éventuelles méthodes #pre_render que les tags de caches sont définitivement attribués.

Dans un monde idéal, le processus aurait été le suivant:

  1. Check pour savoir si l'entité est élligible;
  2. Attribution d'un callback #post_render pour chaque champ;
  3. Attribution d'un callback #post_render pour le tableau complet;
  4. Passage dans le(s) callback(s) #post_render au niveau des champs, exécution du parsing et mise en mémoire des identifiants d'entité internal_link qui ont effectivement remplacés des mots;
  5. Passage dans le callback #post_render au niveau du tableau complet, contrôle pour savoir si des entités internal_link sont concernées, et si c'est le cas, ajout des tags nécessaires, n'incluant QUE les entités traitées;

Dans cette situation, nous aurions eu un procédé idéal car le système de mise en cache aurait invalidé nos contenus uniquement lorsqu'une modification était effectuée sur un lien interne "référencé" par les contenus. Si par exemple l'article 1 ne contenait pas de lien interne X, alors une modification du lien interne X n'aurait pas invalidé le cache de rendu de l'article 1.

Mais comme les tags sont affectés de manière définitive avant l'exécution des callback #post_render, nous devons être plus large dans leur intégration.

Implémentation

Voici la méthode internal_link_entity_view modifiée pour intégrer la logique d'invalidation des caches:

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) {
      $apply_tags = FALSE;
      $fields = $info->getSettings()->getFields();
 
      foreach ($fields as $field_name => $field) {
        if (isset($build[$field])) {
 
          $apply_tags = TRUE;
          // 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';
        }
      }
 
      // Prepare and add tags.
 
      // For any, add ALL tag. This tag can be
      // used when an existing internal link in
      // both mode is deleted.
      $tags = [
        'internal_link:all',
        'internal_link:'.$entity->getEntityTypeId().'-'.$entity->bundle().'-all',
      ];
 
      switch ($info->getMode()) {
 
        case INTERNAL_LINK_MODE_AUTO :
          // AUTO tag can be used when an internal
          // link is added, updated or delete for
          // any entity in automatic mode.
          $tags[] = 'internal_link:auto';
          break;
 
        case INTERNAL_LINK_MODE_MANUAL :
          // {ID} tag can be ued when an existing
          // internal link is edited or deleted.
          $ids = array_keys($info->getEntities());
          $tags = array_merge($tags, array_map(function($value) { return 'internal_link:' . $value; }, $ids));
          break;
      }
      $build['#cache']['tags'] = array_merge($build['#cache']['tags'], $tags);
    }
  }
}

La base de la méthode reste la même que lors de l'article précédent. C'est ensuite que nous traitons l'ajout de tags.

Nous commençons par rajouter 2 tags, peu importe la situation:

      $tags = [
        'internal_link:all',
        'internal_link:'.$entity->getEntityTypeId().'-'.$entity->bundle().'-all',
      ];

Le premier permettra une invalidation "totale" de tous les rendus utilisant internal_link. Le second permettra une invalidation par type d'entité/bundle, si ça s'avère nécessaire. (par exemple dans le cas ou la configuration globale serait modifiée et qu'on désactiverait un bundle particulier)

Nous traitons ensuite les tags selon le mode "internal_link" utilisé par l'entité. Soit elle est en mode automatique car le mode manuel n'est pas actif ou alors aucun lien interne n'a été sélectionné par l'auteur, soit elle est en mode manuel car au moins un lien a été sélectionné par l'auteur.

Dans le mode automatique, nous ajoutons simplement un tag "internal_link:auto" qui permettra d'invalider tous les contenus qui seraient en mode automatique (par exemple lors d'un changement de lien dans ce mode):

$tags[] = 'internal_link:auto';

Dans le mode manuel, nous ajoutons les identifiants de toutes les entités sélectionnées par l'auteur:

$ids = array_keys($info->getEntities());
$tags = array_merge($tags, array_map(function($value) { return 'internal_link:' . $value; }, $ids));

Nous rajoutons enfin la liste des tags dans la clé #cache de notre tableau de rendu afin que nos tags personnalisés soient inclu dans la table de cache:

$build['#cache']['tags'] = array_merge($build['#cache']['tags'], $tags);

Un tag standard d'entité étant déterminé par la paire [entity-type]:[entity-id], nous n'avons rien besoin de plus pour que les caches de rendu concernés soient invalidés lors d'une modification d'un lien interne.

Concrètement, dès lors que le lien interne possédant l'identifiant 123 est édité, le système de cache déclenche l'invalidation sur tout ce qui contiendrait la chaîne "internal_link:123". Ce qui permet donc d'invalider de manière automatisée tout les rendus auxquels nous aurions ajouté cette chaîne.

Nous avons donc un système qui est fonctionnel de manière simplifiée avec les entités en mode manuel. Mais qu'en est-il du mode automatique ? C'est là que la méthode getCacheTagsToInvalidate entre en jeu. Cette méthode est déclarée par la classe "Entity", dont hérite notre classe InternalLink. Nous pouvons donc la surdéfinir dans le fichier src/Entity/InternalLink.php:

  /**
   * {@inheritdoc}
   */
  public function getCacheTagsToInvalidate() {
 
    $tags = parent::getCacheTagsToInvalidate();
 
    switch ($this->getMode()) {
      case INTERNAL_LINK_MODE_MANUAL :
        // Do nothing. Parent method already added
        // the correct cache tag like internal_link:[ID].
        break;
 
      case INTERNAL_LINK_MODE_ALL :
      case INTERNAL_LINK_MODE_AUTO :
        // In other cases, we must add a tag to invalidate
        // any content that use automatic mode.
        $tags[] = $this->entityTypeId . ':auto';
        break;
    }
    return $tags;
  }

Nous faisons d'abord appel à la méthode parent, afin que les tags par défaut soient intégrés. Nous testons ensuite le mode de notre lien.

Si il est en mode manuel, nous n'avons rien besoin de faire puisqu'il est sensé être ajouté via notre implémentation de hook_entity_view.

Si il est dans un autre mode, alors nous devons faire en sorte de rajouter la chaîne "internal_link:auto" dans la liste des tags à invalider lors du retour de cette méthode.

Conclusion

Notre module exploite maintenant la nouvelle API de cache de Drupal 8 afin de gagner en performance sans perdre en efficacité.

Dans le dernier article de la série, nous ferons une conclusion globale sur tout le travail effectué, ainsi qu'une liste d'améliorations qui pourraient être apportées à notre module.

Ajouter un commentaire

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