Nette Caching

Le cache accélère votre application en conservant des données dont l'obtention a été coûteuse, pour y accéder plus vite la prochaine fois. Nous allons voir :

  • comment utiliser le cache
  • comment changer de stockage
  • comment invalider correctement le cache

L'utilisation du cache dans Nette est très simple, tout en couvrant des besoins de mise en cache sophistiqués. Il est conçu pour la performance et une durabilité de 100 %. Il embarque des adaptateurs pour les stockages les plus répandus. Il prend en charge l'invalidation par tags, l'expiration dans le temps, la protection contre le cache stampede, et bien plus.

Installation

Téléchargez et installez le paquet à l'aide de Composer :

composer require nette/caching

Utilisation de base

L'élément central du travail avec le cache est l'objet Nette\Caching\Cache. Nous en créons une instance en passant au constructeur un objet de stockage. Cet objet représente l'endroit physique où les données seront conservées (base de données, Memcached, fichiers sur le disque, etc.). Vous obtenez d'ordinaire l'objet de stockage par injection de dépendances en demandant le type Nette\Caching\Storage. Vous apprendrez l'essentiel dans la section Stockages.

Dans la version 3.0, l'interface portait encore le préfixe I, son nom était donc Nette\Caching\IStorage. De plus, les constantes de la classe Cache s'écrivaient en majuscules, par ex. Cache::EXPIRE au lieu de Cache::Expire.

Dans les exemples qui suivent, supposons que nous avons un alias Cache et une instance de stockage dans la variable $storage.

use Nette\Caching\Cache;

$storage = /* ... */; // instance of Nette\Caching\Storage

Le cache est essentiellement un magasin clé-valeur : nous y lisons et écrivons à l'aide de clés, comme dans un tableau associatif. Or une application se compose de plusieurs parties indépendantes. Si toutes utilisaient un seul stockage (imaginez un unique répertoire sur le disque), des collisions de clés finiraient par se produire. Nette Framework résout cela en découpant l'espace de stockage en espaces de noms (conceptuellement, des sous-répertoires). Chaque partie de l'application travaille alors dans son propre espace de noms au nom unique, et aucune collision n'est possible.

Indiquez le nom de l'espace de noms en second argument du constructeur de la classe Cache :

$cache = new Cache($storage, 'Full Html Pages');

Au besoin, vous pouvez dériver d'une instance existante un nouveau cache limité à un sous-espace de noms avec la méthode derive() :

$subCache = $cache->derive('Images');

Nous pouvons désormais utiliser l'objet $cache pour lire dans le cache et y écrire. La méthode load() sert aux deux. Le premier argument est la clé, le second un callback PHP appelé si la clé n'est pas trouvée dans le cache. Le callback produit la valeur, la renvoie, et la méthode load() la met en cache :

$value = $cache->load($key, function () use ($key) {
	$computedValue = /* ... */; // expensive computation
	return $computedValue;
});

Si le second paramètre est omis ($value = $cache->load($key)), load() renvoie null quand l'élément n'est pas trouvé dans le cache.

Il est appréciable que toute structure sérialisable puisse être mise en cache, pas seulement les chaînes. Cela vaut aussi pour les clés.

Pour supprimer un élément du cache, utilisez la méthode remove() :

$cache->remove($key);

Vous pouvez aussi enregistrer un élément dans le cache avec la méthode $cache->save($key, $data, ?array $dependencies = null). Cela dit, l'approche par load() montrée plus haut est généralement préférable.

Mémoïsation

La mémoïsation consiste à mettre en cache le résultat d'un appel de fonction ou de méthode, si bien qu'au prochain appel avec les mêmes arguments, le résultat en cache est renvoyé au lieu d'être recalculé.

Les méthodes et fonctions peuvent être appelées de façon mémoïsée avec call(callable $callback, ...$args) :

$result = $cache->call('gethostbyaddr', $ip);

La fonction gethostbyaddr() n'est ainsi appelée qu'une seule fois pour chaque argument $ip distinct. Les appels suivants avec le même $ip renverront la valeur en cache.

Il est également possible de créer autour d'une méthode ou d'une fonction un emballage mémoïsé, appelable plus tard :

function factorial($num)
{
	return /* ... */;
}

$memoizedFactorial = $cache->wrap('factorial');

$result = $memoizedFactorial(5); // calculates it the first time
$result = $memoizedFactorial(5); // returns from cache the second time

Expiration et invalidation

Quand on utilise le cache, il faut répondre à la question du moment où les données enregistrées cessent d'être valides. Nette Framework offre des mécanismes pour limiter la validité des données ou les supprimer explicitement (ce que la terminologie du framework appelle “invalidation”).

La validité des données se définit au moment de l'enregistrement, d'ordinaire par le troisième paramètre de la méthode save(), par ex. :

$cache->save($key, $value, [
	$cache::Expire => '20 minutes',
]);

Elle peut aussi se définir par le paramètre $dependencies passé par référence au callback de la méthode load(), par ex. :

$value = $cache->load($key, function (&$dependencies) {
	$dependencies[Cache::Expire] = '20 minutes';
	return /* ... */;
});

Ou encore par le 3e paramètre de la méthode load() elle-même, par ex. :

$value = $cache->load($key, function () {
	return /* ... */;
}, [Cache::Expire => '20 minutes']);

Dans les exemples qui suivent, nous partirons de la deuxième variante, avec la variable $dependencies dans le callback.

Expiration

La forme la plus simple d'expiration est la limite de temps. Ceci met les données en cache avec une validité de 20 minutes :

// accepts number of seconds or a UNIX timestamp as well
$dependencies[Cache::Expire] = '20 minutes';

Si vous voulez que la durée de validité se prolonge à chaque lecture (expiration glissante), vous pouvez procéder ainsi, en sachant que cela alourdit le cache :

$dependencies[Cache::Sliding] = true;

Une option utile est de faire expirer les données lorsqu'un fichier précis, ou l'un de plusieurs fichiers, est modifié. C'est pratique par exemple quand on met en cache des données issues du traitement de ces fichiers. Utilisez des chemins absolus.

$dependencies[Cache::Files] = '/path/to/data.yaml';
// or
$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];

Nous pouvons faire expirer un élément du cache quand un autre élément précis (ou l'un de plusieurs autres) expire. C'est utile lorsqu'on met en cache, par exemple, une page HTML entière et ses fragments sous des clés différentes. Quand un fragment change, toute la page doit être invalidée. Si les fragments sont stockés sous les clés frag1 et frag2, utilisez :

$dependencies[Cache::Items] = ['frag1', 'frag2'];

L'expiration peut aussi être pilotée par des fonctions ou des méthodes statiques à vous. Elles sont appelées à chaque lecture pour déterminer si l'élément est encore valide. Nous pouvons par exemple faire expirer un élément dès que la version de PHP change. Créez une fonction qui compare la version actuelle avec un paramètre, et à l'enregistrement, ajoutez aux dépendances un tableau de la forme [nom de la fonction, ...arguments] :

function checkPhpVersion($ver): bool
{
	return $ver === PHP_VERSION_ID;
}

$dependencies[Cache::Callbacks] = [
	['checkPhpVersion', PHP_VERSION_ID] // expire when checkPhpVersion(...) === false
];

Tous ces critères peuvent naturellement être combinés. L'élément du cache expire dès qu'au moins un critère n'est plus rempli.

$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';

Invalidation par tags

Les tags offrent un mécanisme d'invalidation très pratique. Nous pouvons attribuer à chaque élément enregistré dans le cache une liste de tags (des chaînes quelconques). Supposons par exemple que nous ayons une page HTML affichant un article et ses commentaires, que nous voulons mettre en cache. À l'enregistrement, nous indiquons les tags correspondants :

$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];

Passons maintenant dans l'administration. Nous y avons un formulaire d'édition des articles. En même temps que l'enregistrement de l'article en base, nous appelons la méthode clean() pour supprimer du cache les éléments portant le tag :

$cache->clean([
	$cache::Tags => ["article/$articleId"],
]);

De même, à l'ajout d'un nouveau commentaire (ou à sa modification), nous devons penser à invalider le tag correspondant :

$cache->clean([
	$cache::Tags => ["comments/$articleId"],
]);

Qu'avons-nous gagné ? Notre cache HTML sera désormais invalidé (supprimé) chaque fois que l'article associé ou ses commentaires changent. À l'édition de l'article d'ID = 10, le tag article/10 est invalidé et la page HTML en cache portant ce tag est supprimée. Il en va de même lorsqu'un nouveau commentaire est ajouté sous l'article concerné.

Les tags nécessitent un Journal.

Invalidation par priorité

Nous pouvons attribuer des priorités aux différents éléments du cache. Cela permet une suppression contrôlée, par exemple quand le cache dépasse une certaine taille :

$dependencies[Cache::Priority] = 50;

Pour supprimer tous les éléments dont la priorité est inférieure ou égale à 100 :

$cache->clean([
	$cache::Priority => 100,
]);

Les priorités nécessitent ce qu'on appelle un Journal.

Vider le cache

Le paramètre Cache::All vide tout :

$cache->clean([
	$cache::All => true,
]);

Lecture en masse

Pour lire et écrire en masse dans le cache, utilisez la méthode bulkLoad(). Passez-lui un tableau de clés et elle renvoie un tableau des valeurs correspondantes :

$values = $cache->bulkLoad($keys);

La méthode bulkLoad() fonctionne comme load() et accepte elle aussi un callback en second paramètre. Ce callback reçoit la clé de l'élément à produire :

$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
	$computedValue = /* ... */; // expensive computation
	return $computedValue;
});

À l'inverse, pour écrire plusieurs éléments d'un coup, utilisez la méthode bulkSave(), qui prend un tableau de paires clé => valeur et des dépendances facultatives :

$cache->bulkSave([
	$key1 => $value1,
	$key2 => $value2,
], [Cache::Expire => '20 minutes']);

Utilisation avec PSR-16

Pour utiliser Nette Cache avec une interface PSR-16, vous pouvez recourir à PsrCacheAdapter. Il permet une intégration sans heurt entre Nette Cache et tout code ou bibliothèque attendant une implémentation de cache compatible PSR-16.

$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);

Vous pouvez désormais utiliser $psrCache comme un cache PSR-16 standard :

$psrCache->set('key', 'value', 3600); // stores the value for 1 hour
$value = $psrCache->get('key', 'default');

L'adaptateur prend en charge toutes les méthodes définies par PSR-16, y compris getMultiple(), setMultiple() et deleteMultiple().

Mise en cache de la sortie

La sortie peut être capturée et mise en cache très élégamment :

if ($capture = $cache->capture($key)) {

	// echo ... printing some data

	$capture->end(); // save the output to the cache
}

Si la sortie est déjà présente dans le cache, la méthode capture() l'affiche et renvoie null : le bloc de la condition if est donc sauté. Sinon, elle commence à mettre la sortie en tampon et renvoie un objet $capture, dont la méthode end() vous sert à enregistrer finalement les données capturées dans le cache.

Dans la version 3.0, cette méthode s'appelait $cache->start().

Mise en cache dans Latte

La mise en cache dans les templates Latte est très simple. Il suffit d'entourer la portion de template à mettre en cache par les tags {cache}...{/cache}. Le cache est automatiquement invalidé dès que le fichier source du template change (y compris tout template inclus dans le bloc mis en cache). Les tags {cache} peuvent être imbriqués. Quand un bloc imbriqué est invalidé (par ex. via un tag), son bloc parent l'est aussi.

Dans le tag, vous pouvez indiquer les clés auxquelles l'entrée du cache sera liée (ici la variable $id), définir une durée d'expiration et fixer des tags d'invalidation.

{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
	...
{/cache}

Tous ces paramètres sont facultatifs : vous n'avez à indiquer ni l'expiration, ni les tags, ni même les clés.

L'usage du cache peut aussi être conditionné par if : le contenu ne sera mis en cache que si la condition est remplie.

{cache $id, if: !$form->isSubmitted()}
	{$form}
{/cache}

Stockages

Un stockage est un objet représentant l'endroit physique où les données sont conservées. Nous pouvons utiliser une base de données, un serveur Memcached, ou le stockage le plus immédiatement disponible : des fichiers sur le disque.

Stockage Description
FileStorage Stockage par défaut, enregistre le cache dans des fichiers sur le disque.
MemcachedStorage Utilise un serveur Memcached pour le stockage.
MemoryStorage Les données sont conservées temporairement en mémoire (perdues à la fin de la requête).
SQLiteStorage Les données sont conservées dans un fichier de base SQLite.
DevNullStorage Les données ne sont en réalité pas conservées ; utile pour les tests.

Vous obtenez l'objet de stockage par injection de dépendances en demandant le type Nette\Caching\Storage. Par défaut, Nette fournit un objet FileStorage qui conserve les données dans le sous-répertoire cache du répertoire des fichiers temporaires.

Vous pouvez changer le stockage par défaut dans la configuration :

services:
	cache.storage: Nette\Caching\Storages\DevNullStorage

FileStorage

Écrit les entrées du cache dans des fichiers sur le disque. Le stockage Nette\Caching\Storages\FileStorage est très optimisé pour la performance et, surtout, garantit l'atomicité complète des opérations. Qu'est-ce que cela signifie ? Avec ce cache, il ne peut pas arriver que vous lisiez un fichier qu'un autre thread n'a pas fini d'écrire, ni que quelqu'un le supprime pendant que vous le lisez. Son utilisation est donc parfaitement sûre.

Ce stockage embarque en outre une fonction importante qui empêche une envolée extrême de la charge CPU quand le cache est vidé ou encore “froid” (c'est-à-dire pas encore constitué). C'est la prévention du cache stampede. Le phénomène survient quand plusieurs requêtes concurrentes demandent en même temps le même élément du cache (par ex. le résultat d'une requête SQL coûteuse). Si l'élément n'est pas en cache à cet instant, tous ces processus peuvent se mettre à exécuter la même opération coûteuse (la requête SQL). La charge du serveur en est démultipliée, et il peut même arriver qu'aucun thread ne réponde dans le temps imparti, que le cache ne se constitue pas et que l'application s'effondre. Heureusement, le cache de Nette gère cela : quand plusieurs requêtes concurrentes portent sur le même élément, seul le premier thread le produit. Les autres attendent, puis utilisent le résultat produit par le premier.

Exemple de création d'un FileStorage :

// the storage will be the directory '/path/to/temp' on disk
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');

MemcachedStorage

Le serveur Memcached est un système distribué et très performant de mise en cache d'objets en mémoire. Son adaptateur dans Nette est Nette\Caching\Storages\MemcachedStorage. Dans la configuration, indiquez l'adresse IP du serveur et son port s'il diffère du 11211 standard.

Nécessite l'extension PHP memcached.

services:
	cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')

MemoryStorage

Nette\Caching\Storages\MemoryStorage est un stockage qui garde les données dans un tableau PHP. Elles sont donc perdues à la fin de la requête.

SQLiteStorage

La base de données SQLite, avec l'adaptateur Nette\Caching\Storages\SQLiteStorage, offre un moyen de mettre les données en cache dans un unique fichier sur le disque. La configuration indique le chemin de ce fichier de base.

Nécessite les extensions PHP pdo et pdo_sqlite.

services:
	cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')

DevNullStorage

Une implémentation particulière est Nette\Caching\Storages\DevNullStorage, qui ne conserve en réalité aucune donnée. Elle convient donc aux tests, quand vous voulez éliminer les effets du cache.

Utilisation du cache dans le code

Pour utiliser le cache dans votre code, il existe deux approches principales. La première consiste à obtenir l'objet de stockage par injection de dépendances, puis à créer vous-même l'objet Cache :

use Nette;

class ClassOne
{
	private Nette\Caching\Cache $cache;

	public function __construct(Nette\Caching\Storage $storage)
	{
		$this->cache = new Nette\Caching\Cache($storage, 'my-namespace');
	}
}

La seconde consiste à se faire passer directement l'objet Cache :

class ClassTwo
{
	public function __construct(
		private Nette\Caching\Cache $cache,
	) {
	}
}

L'objet Cache doit alors être défini dans la configuration, par exemple ainsi :

services:
	- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )

Journal

Nette conserve les informations sur les tags et les priorités dans ce qu'on appelle un journal. SQLite est utilisé par défaut à cette fin, via le fichier journal.s3db, et les extensions PHP pdo et pdo_sqlite sont requises.

Vous pouvez changer l'implémentation du journal dans la configuration :

services:
	cache.journal: MyJournal

Services DI

Ces services sont ajoutés au conteneur DI :

Nom Type Description
cache.journal Nette\Caching\Storages\Journal Le stockage du journal du cache
cache.storage Nette\Caching\Storage Le stockage principal du cache

Désactiver le cache

Une façon de désactiver la mise en cache dans votre application est de régler le stockage sur DevNullStorage :

services:
	cache.storage: Nette\Caching\Storages\DevNullStorage

Ce réglage n'affecte ni la mise en cache des templates Latte ni celle du conteneur DI, car ces bibliothèques n'utilisent pas les services de nette/caching et gèrent leur cache par elles-mêmes. Par ailleurs, il n'est en général pas nécessaire de désactiver leur cache en mode développement.

Si vous passez à une version plus récente, consultez la page mise à niveau.

version: 3.x