Aller au contenu principal
Aller au contenu principal

Bonjour, je suis Karim Boudjema. Développeur back-end senior établi à Montréal, au Canada, passionné par Drupal, l'IA et les tests automatisés.

Le piège du cache tag 'node_list' dans les vues en Drupal 11

Le cache tag node_list est généré automatiquement dès que nous construisons une vue qui liste des nodes. Il invalide le cache de toutes les vues qui listent n'importe quelle sorte de node (page, article, et ainsi de suite) dès que nous réalisons une opération CUD (create, update, delete) sur un node, quel qu'il soit.

Par défaut, c'est une excellente stratégie d'invalidation : modifiez un node et chaque liste de nodes se rafraîchit pour le refléter. Et oui, la Cache API de Drupal est vraiment remarquable. Merci à tous les contributeurs, comme Wim Leers, qui ont rendu cela possible.

Jusqu'ici tout va bien, mais... que se passe-t-il sur un site à fort trafic avec des dizaines de types de nodes différents et des centaines de vues qui les affichent ? Si nous modifions un seul node (disons une page), le cache de toutes les vues qui listent des pages est invalidé, mais aussi celui de toutes les vues qui listent des articles, et de tout autre bundle. Modifiez une page et les listes d'articles sont jetées elles aussi, alors qu'aucun article n'a changé. Sur un site chargé, aux modifications fréquentes, comme un journal en ligne, cela devient un vrai problème de performance.

Comment invalider le cache des vues de manière plus précise, seulement pour les nodes du même type ?

Nous allons procéder en deux étapes :

  1. placer un cache tag personnalisé sur chaque vue, pour le type de node qu'elle liste, comme node:type:page pour les vues qui listent des pages, node:type:article pour les vues qui listent des articles, et ainsi de suite ;
  2. invalider ce cache tag personnalisé quand une opération CUD survient sur ce type de node précis, avec un hook_node_presave().

1. Ajouter des cache tags personnalisés avec le module Views Custom Cache Tags

Heureusement, la communauté a déjà résolu la première moitié pour nous : le module contrib Views Custom Cache Tags nous permet de placer un cache tag personnalisé sur n'importe quelle vue. Dans notre cas, nous en placerons un par type de node :

  • node:type:article sur les vues qui listent des articles,
  • node:type:page sur les vues qui listent des pages,
  • node:type:<votre-type-de-node> et ainsi de suite.

Tout d'abord, téléchargeons et installons le module. Drupal Console n'existe plus, nous utilisons donc Composer et Drush :

composer require drupal/views_custom_cache_tag
drush en views_custom_cache_tag

Créons ensuite deux vues de type block pour essayer, une qui liste cinq articles (nommée Block Articles) et une qui liste cinq pages (Block Pages). Plaçons maintenant le cache tag personnalisé sur la vue des articles : éditez la vue, ouvrez la section Advanced, et sous Caching passez en Tag based. Choisissez ensuite Custom Tag based et saisissez le tag pour ce type de node : comme cette vue liste des articles, nous saisissons node:type:article. Pour une vue qui liste un autre bundle, nous saisirions node:type:<node-type>. N'oubliez pas de cliquer sur Apply, puis d'enregistrer la vue. Faites de même sur la vue des pages avec node:type:page.

Une fois que chaque vue qui liste des nodes porte son cache tag personnalisé, nous passons à la deuxième étape : invalider ces tags au bon moment.

2. Invalider les cache tags personnalisés avec un hook node presave

Nous invalidons maintenant le cache tag personnalisé chaque fois qu'une opération CUD survient sur un type de node précis, avec hook_node_presave(). Et oui, il y a encore des hooks en Drupal 11, mais la façon de les écrire a changé. En Drupal 8, cela vivait comme une fonction procédurale dans un fichier .module ; en Drupal 11, la forme moderne est un hook orienté objet : une méthode sur une classe placée sous src/Hook/, marquée avec l'attribut PHP #[Hook]. L'exemple original se trouve sur github.com/KarimBoudjema/Drupal8-invalidate-custom-cache-tags ; le voici réécrit pour Drupal 11.

Créons le module (il dépend du module contrib) :

drush generate module
drush en kb_invalidate_custom_cache_tags

Puis ajoutons la classe de hook dans src/Hook/NodeCacheHooks.php :

<?php

declare(strict_types=1);

namespace Drupal\kb_invalidate_custom_cache_tags\Hook;

use Drupal\Core\Cache\Cache;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\node\NodeInterface;

/**
 * Invalide un cache tag personnalisé par bundle lorsqu'un node est enregistré.
 */
final class NodeCacheHooks {

  /**
   * Implémente hook_ENTITY_TYPE_presave() pour le type d'entité node.
   */
  #[Hook('node_presave')]
  public function nodePresave(NodeInterface $node): void {
    // Construire le tag par bundle : node:type:article, node:type:page, ...
    $cacheTag = 'node:type:' . $node->bundle();
    // Le marquer comme invalide dans tous les bins de cache.
    Cache::invalidateTags([$cacheTag]);
  }

}

Ce hook se déclenche chaque fois qu'un node est créé ou mis à jour. Nous lisons le bundle du node avec $node->bundle() (article, page, et ainsi de suite) et construisons le tag node:type:<bundle>, exactement le tag que nous avons placé sur les vues correspondantes. Ensuite, Cache::invalidateTags() marque ce seul tag comme invalide dans tous les bins de cache, si bien que seules les vues qui le portent sont reconstruites.

Ainsi, si nous insérons ou mettons à jour un article, le tag est node:type:article et seules les vues qui listent des articles sont invalidées. Les vues avec node:type:page restent valides. C'est justement ce que nous cherchions. (Cache::invalidateTags() est un raccourci statique ; dans une classe qui injecte déjà des services, vous pourriez utiliser le service cache_tags.invalidator à la place.)

En résumé. Pour éviter que toutes les vues de nodes soient invalidées au moindre changement d'un node, nous avons remplacé le cache tag large node_list par un tag personnalisé plus étroit, node:type:<node-type>. Le module Views Custom Cache Tags nous permet de placer ce tag sur chaque vue selon le bundle qu'elle liste (node:type:article, node:type:page, et ainsi de suite). Puis nous invalidons le bon tag à l'enregistrement avec un hook_node_presave(), écrit à la manière de Drupal 11 sous forme de classe #[Hook] orientée objet.

Voilà ! Si vous avez une autre stratégie pour le problème du node_list, partagez-la avec nous dans les commentaires.

Pour en savoir plus