Implémentation de la gestion du cache dans Drupal : Une approche terre-à-terre (3/4)
Quelques notions importantes pour la gestion fine du cache Drupal.

Après les deux articles précédents sur les différentes stratégies de cache et leur stockage, disponibles sur le blog Medium ekino-France, j’ai maintenant une vision plus claire de ce qui se passe dans les coulisses des modules de cache Drupal. Je vais maintenant prendre un instant pour examiner la gestion du cache depuis une application Drupal, par exemple, depuis un Controller ou un Block de rendu.
En plus de la documentation Drupal, il existe de nombreux tutoriels sur le web qui expliquent comment parvenir à bien gérer son cache. Mon ambition n’est pas ici de refaire et encore moins de faire mieux. Néanmoins, je trouve que certaines notions et terminologies, utiles pour manipuler le cache dans notre application, ne sont pas forcément aisées à maîtriser (en tout cas ça ne l’a pas été pour moi). Je vais donc essayer de reformuler, avec mes mots, ces notions sans véritablement approfondir leur utilisation. Aborder ces notions nous sera aussi utile par la suite pour discuter d’autres aspects du fonctionnement du cache Drupal.
Cache Metadata
Le premier concept est celui de métadonnées de cache. Elles vont nous permettre de créer et de contrôler le cycle de vie du cache. Il y a trois types de métadonnées de cache : “cache-max-age”, “cache-tags” et “cache-contexts”
— Cache max-age —
Le Max-Age va contrôler la durée de validité du cache. Cette métadonnée fait echo à la valeur “max-age=” de l’entête HTTP standard “cache-control”. Comme attendu, si cette durée, qui sera exprimée en seconde à partir de la création du cache, est dépassée, il sera immédiatement invalidé lors de sa récupération.
— Cache tags —
Les cache-tags sont des “étiquettes” pouvant être accolées à un cache. Il n’y a pas de liste finie. Il est totalement possible d’apposer nos propres étiquettes sur un cache pour l’invalider nous-même lors d’une action arbitraire.
Car c’est là la principale fonction des cache-tags : gérer finement l’invalidation du cache.
Ci-dessous un exemple de déclaration d’un cache-tag custom :
// Custom cache tag example
// In a block class file
final class MyBlock extends BlockBase
{
public function getCacheTags() {
return [
'my_cache_tag',
// other tags ...
];
}
}
// Somewhere else, where you need to invalidate cache
function my_module_some_hook_for_custom_clear_cache_logic(){
// ...
DrupalCoreCacheCache::invalidateTags(['my_cache_tag']);
// ...
}
Drupal gère lui-même des cache-tags pour les cas d’usages standards lors des principales opérations du CMS. Cette fonctionnalité native permet de rafraîchir le cache dès qu’un évènement d’invalidation intervient. Par exemple, lors de la modification d’une entité (EntityBase ou toute instance apparentée) qui est sauvegardée en base de données. Ainsi Drupal va s’occuper pour nous des cache-tags déclarés avec le format :
- <entity_type_id>:<entity_id> qui nous permettra d’invalider du cache lorsqu’une modification aura lieu sur une entité définie.
- <entity_type_id>_list et sa variante <entity_type_id>_list:<bundle> vont nous permettre d’invalider le cache lorsque n’importe quelle entité pour un type spécifié sera modifiée.
- config:<configuration name> nous permettra d’invalider le cache lorsqu’une modification aura lieu pour une configuration donnée.
— Cache contexts —
Les cache-contexts seront principalement utiles pour générer et récupérer le cache par rapport au contexte spécifique de la requête. Ils agissent comme une bulle de cache pour un contexte particulier. Il existe de très nombreux contextes pris en charge nativement par Drupal.
Voici une liste, avec quelques explications, de ces contextes disponibles de base avec Drupal. Certains de ces cache-contexts permettent une bonne personnalisation du cache :
- cookies : la liste des cookies disponibles sera utilisée comme marqueur pour gérer le cache. On peut aussi se baser sur la valeur d’un cookie en particulier avec cookies:nom_du_cookie
- headers : sans surprise, pour gérer le cache selon les entêtes de la requête ou pour le contenu d’un entête particulier (headers:nom_du_ header)
- ip : pour gérer le cache par IP, le seul cas d’usage que je peux imaginer est celui d’environnements contrôlés, type intranet…
- languages : offre deux possibilités (pas trop documentées), “languages:language_content” pour gérer le cache selon la langue utilisée pour le rendu du contenu (par exemple la traduction d’un Node) et “languages:language_interface” pour gérer le cache selon la langue sélectionnée pour l’affichage de l’interface (qui est donc conservée dans la configuration). Ce dernier est un des contextes utilisés par défaut.
- protocol_version : pour gérer le cache selon le protocole http (HTTP/1.1 ou HTTP/2.0 par exemple).
- request_format : gérer le cache pour le html ou pour un autre format (json, xml…).
- route : selon le type de route, plusieurs possibilités pour ce contexte dont le nom de la route.
- session : selon l’existence ou non de la session ou même par session !
- theme : gestion du cache selon le thème du site qui est installé. C’est un des contextes apposés par défaut sur une requête.
- timezone : cela peut permettre de gérer les caches pour des composants avec un affichage en heure locale.
- url : pour gérer le cache selon l’url, le chemin ou juste les paramètres. Cela permet d’avoir plusieurs gestions de caches pour une même page ou un seul cache pour plusieurs sous-domaines.
- user : pour gérer le cache selon les rôles, les permissions, par utilisateur, etc. Le contexte “user.permissions” fait partie des contextes par défaut.
Il est aussi possible de créer ses propres contextes si nécessaire. Pour plus de détails, voir la documentation des cache-contexts.
Contextes déclarés “incachables”
De base, Drupal déclare les contextes “session” et “user” incachables, c’est à dire qui ne seront jamais récupérés dans le stockage du cache. Il est possible de modifier ce comportement en ajoutant et supprimant des contextes. Pour cela, il faudra surcharger la configuration auto_placeholder_conditions de l’entrée renderer.config. Cette entrée est déclarée ainsi dans le fichier “core.services.yml” :
# core.services.yml
renderer.config:
auto_placeholder_conditions:
max-age: 0
contexts: ['session', 'user']
tags: []
À noter qu’on peut aussi déclarer dans cette configuration des cache-tags qui rendront des éléments incachables !
Limite du cache-context
Le cache contexte ne fonctionne pas avec Internal Page Cache. Si on souhaite utiliser un contexte avec des utilisateurs anonymes, il faut forcer l’utilisation du Dynamic Cache Page.
Par ailleurs, Drupal réalise l’optimisation du cache-context et supprime les contextes identifiés comme redondants, en privilégiant le contexte le plus global. Par exemple, l’optimisation retirera user.permissions de la liste de contextes [user, user.permissions]. Or si le contexte optimisé se voit retirer un contexte modifiable en configuration, le cache peut devenir incohérent car les changements de configuration ne seront pas pris en compte pour invalider ce cache. Il est recommandé dans ce cas d’utiliser un mélange de caches-contexts et de cache-tags ciblant la configuration (config:<configuration name>).
A noter qu’à partir de Drupal 10.2 le cache-context est disponible pour les caches autre que celui du rendu, ce qui n’était pas le cas avant (voir https://www.drupal.org/node/3365546).
Cache Dependency
Il s’agit de rendre un cache dépendant d’un autre élément qui a un niveau de mise en cache différent. L’invalidation du cache de la dépendance entraînera en cascade l’invalidation du rendu qui en dépend.
Par exemple, imaginons un bloc de rendu qui contient un nom d’utilisateur avec le nombre d’amis avec lequel il est connecté entre parenthèses. Le block pourra être mis en cache sur le nombre d’amis qui est calculé (et peut facilement varier) avec une dépendance sur le nom d’utilisateur (qui normalement ne change pas souvent mais c’est une possibilité).
Pour la partie plus technique, les objets qui acceptent une dépendance devront implémenter RefinableCacheableDependencyInterface. Un certain nombre de classes Drupal le font déjà comme la classe abstraite EntityBase.
Nous pouvons ajouter une dépendance de cache en utilisant la méthode addCacheableDependency (qui peut être implémentée avec le trait PHP RefinableCacheableDependencyTrait,). Il faudra lui passer comme argument une instance de la CacheableDepencyInterface. A noter que le paramètre de addCacheableDependency n’est pas typé et on peut lui passer dans les faits n’importe quoi… cependant tout ce qui n’implémente pas CacheableDependencyInterface est considéré comme ne pouvant être mis en cache dans l’implémentation de Drupal.
S’il on souhaite réaliser notre propre implémentation de la dépendance de cache sur une entité personnalisée, on peut s’appuyer sur trois traits PHP disponibles dans le core Drupal :
– CacheableDependencyTrait qui met à disposition une implémentation de la CacheableDependecyInterface. On y trouve aussi la méthode setCacheability, qui a une visibilité “protégée”, pour configurer le niveau de cache depuis l’entité, par exemple dans le constructeur.
– RefinableCacheableDependencyTrait qui ajoute l’implémentation pour empiler les dépendances à mettre en cache, comme attendu par l’interface RefinableCacheableDependencyInterface.
– UnchangingCacheableDependencyTrait : ce trait permet d’implémenter rapidement la CacheableDepencyInterface pour un objet dont le cache doit être considéré comme permanent.
Vous pouvez trouver, en complément, une explication du concept des dépendances de cache dans la documentation Drupal sur les cacheable dependencies.
Cache bins
S’il on comparait le stockage du cache à une commode, les cache-bins seraient les tiroirs !
Ils sont utiles pour séparer le cache entre modules ou selon sa finalité. On peut les déclarer dans la configuration de l’application (via la variable $settings). Cela permet de modifier le comportement des cache-bins existants ou d’en ajouter.
Chaque Cache bin est déclaré séparément et ils peuvent donc utiliser un système de stockage (cache backend) différent.
# exemple de la déclaration d'un cache bin dans le core.service.yml
cache.config:
class: DrupalCoreCacheCacheBackendInterface
tags:
- { name: cache.bin, default_backend: cache.backend.chainedfast }
Lorsque l’on déclare la cacheabilty d’un élément, dans un render array par exemple, on peut aussi assigner à un cache-bin spécifique s’il on souhaite modifier le comportement par défaut.
Il y a de nombreux bins et vous pouvez en créer de nouveau mais pour le rendu de page nous pouvons citer :
- Le cache_render : conserve des composants qui ont été rendus.
- Le dynamic_page_cache : conserve un rendu partiel avec placeholders qui pourront éventuellement être récupérés dans le cache_render.
- Le cache_page : conserve les réponses rendues pour l’Internal cache page.
- Au niveau du rendu, d’autres bins sont sollicités comme le cache_access_policy, le cache_config, le cache_menu, etc.
Il y a encore plein d’autres cache-bins qui peuvent conserver des données d’entités, de compilation du container de services, etc.
Cache keys
Il s’agit de clés arbitraires qui servent à identifier un cache plus précisément à l’intérieur d’un cache-bin. Elles sont fixées par les services Drupal ou les modules. Elles peuvent aussi être déclarées manuellement en cas de besoins spécifiques.
Avec ces trois premières parties, je commence pour ma part à avoir une meilleure compréhension du fonctionnement du cache dans Drupal. Toutefois, avant de conclure, j’étais curieux d’approfondir la génération du rendu du cache. C’est ce que nous allons voir dans la dernière partie.
Implémentation de la gestion du cache dans Drupal : Une approche terre-à-terre (3/4) was originally published in ekino-france on Medium, where people are continuing the conversation by highlighting and responding to this story.