Mise en cache

La mise en cache accélère votre application en stockant les données – une fois qu'elles ont été récupérées – pour une utilisation future. Nous allons vous montrer :

  • Comment utiliser le cache
  • Comment modifier le stockage du cache
  • Comment invalider correctement le cache

L'utilisation du cache est très simple dans Nette, mais elle couvre également des besoins de cache très avancés. Il est conçu pour la performance et une durabilité à 100%. Fondamentalement, vous trouverez des adaptateurs pour le stockage backend le plus courant. Il permet l'invalidation basée sur les balises, la protection du cache, l'expiration du temps, etc.

Installation

Téléchargez et installez le paquet en utilisant Composer:

composer require nette/caching

Utilisation de base

Le centre du travail avec le cache est l'objet Nette\Caching\Cache. Nous créons son instance et passons en paramètre au constructeur ce que l'on appelle le stockage. Il s'agit d'un objet représentant l'endroit où les données seront physiquement stockées (base de données, Memcached, fichiers sur disque, …). Vous obtenez l'objet storage en le passant en utilisant l'injection de dépendance avec le type Nette\Caching\Storage. Vous découvrirez tous les éléments essentiels dans la section Storage.

Dans la version 3.0, l'interface avait toujours le type I prefix, so the name was Nette\Caching\IStorage. De plus, les constantes de la classe Cache prenaient la majuscule, donc par exemple Cache::EXPIRE au lieu de Cache::Expire.

Pour les exemples suivants, supposons que nous ayons un alias Cache et un stockage dans la variable $storage.

use Nette\Caching\Cache;

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

Le cache est en fait un magasin de clés-valeurs, nous lisons et écrivons donc les données sous les clés comme dans les tableaux associatifs. Les applications sont constituées d'un certain nombre de parties indépendantes, et si elles utilisaient toutes un seul stockage (par exemple, un répertoire sur un disque), il y aurait tôt ou tard une collision de clés. Le Framework Nette résout le problème en divisant l'espace entier en espaces de noms (sous-répertoires). Chaque partie du programme utilise alors son propre espace avec un nom unique et aucune collision ne peut se produire.

Le nom de l'espace est spécifié comme deuxième paramètre du constructeur de la classe Cache :

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

Nous pouvons maintenant utiliser l'objet $cache pour lire et écrire dans le cache. La méthode load() est utilisée pour les deux. Le premier argument est la clé et le second est le callback PHP, qui est appelé lorsque la clé n'est pas trouvée dans le cache. Le callback génère une valeur, la renvoie et la met en cache :

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

Si le deuxième paramètre n'est pas spécifié $value = $cache->load($key), la valeur null est retournée si l'élément n'est pas dans le cache.

Ce qui est bien, c'est que toutes les structures sérialisables peuvent être mises en cache, pas seulement les chaînes de caractères. Et il en va de même pour les clés.

L'élément est effacé du cache à l'aide de la méthode remove():

$cache->remove($key);

Vous pouvez également mettre un élément en cache en utilisant la méthode $cache->save($key, $value, array $dependencies = []). Cependant, la méthode ci-dessus utilisant load() est préférable.

Mémorisation

La mémorisation consiste à mettre en cache le résultat d'une fonction ou d'une méthode afin de pouvoir l'utiliser la prochaine fois au lieu de calculer la même chose encore et encore.

Les méthodes et les fonctions peuvent être appelées mémoïsées en utilisant call(callable $callback, ...$args):

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

La fonction gethostbyaddr() n'est appelée qu'une seule fois pour chaque paramètre $ip et la fois suivante, la valeur du cache sera retournée.

Il est également possible de créer une enveloppe mémorisée pour une méthode ou une fonction qui peut être appelée ultérieurement :

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

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

$result = $memoizedFactorial(5); // le comptabilise
$result = $memoizedFactorial(5); // le retourne du cache

Expiration et invalidation

Avec la mise en cache, il est nécessaire d'aborder la question de l'invalidation de certaines des données précédemment enregistrées au fil du temps. Nette Framework fournit un mécanisme permettant de limiter la validité des données et de les supprimer d'une manière contrôlée (“les invalider”, selon la terminologie du framework).

La validité des données est définie au moment de l'enregistrement en utilisant le troisième paramètre de la méthode save(), par exemple :

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

Ou à l'aide du paramètre $dependencies passé par référence à la callback de la méthode load(), par ex :

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

Ou en utilisant le 3ème paramètre de la méthode load(), par exemple :

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

Dans les exemples suivants, nous supposerons la deuxième variante et donc l'existence d'une variable $dependencies.

Expiration

L'expiration la plus simple est la limite de temps. Voici comment mettre en cache des données valables pendant 20 minutes :

// il accepte également le nombre de secondes ou le timestamp UNIX
$dependencies[Cache::Expire] = '20 minutes';

Si nous voulons prolonger la période de validité à chaque lecture, c'est possible de cette façon, mais attention, cela augmentera l'overhead du cache :

$dependencies[Cache::Sliding] = true;

L'option la plus pratique est la possibilité de laisser les données expirer lorsqu'un fichier particulier est modifié ou l'un de plusieurs fichiers. Cela peut être utilisé, par exemple, pour mettre en cache les données résultant de la procession de ces fichiers. Utilisez des chemins absolus.

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

On peut laisser un élément du cache expirer lorsqu'un autre élément (ou un parmi plusieurs autres) expire. Cela peut être utilisé lorsque nous mettons en cache la page HTML entière et des fragments de celle-ci sous d'autres clés. Dès que le fragment change, la page entière devient invalide. Si nous avons des fragments stockés sous des clés telles que frag1 et frag2, nous utiliserons :

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

L'expiration peut également être contrôlée à l'aide de fonctions personnalisées ou de méthodes statiques, qui décident toujours à la lecture si l'élément est toujours valide. Par exemple, nous pouvons laisser l'élément expirer lorsque la version de PHP change. Nous allons créer une fonction qui compare la version actuelle avec le paramètre, et lors de la sauvegarde, nous ajouterons un tableau de la forme [function name, ...arguments] aux dépendances :

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

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

Bien sûr, tous les critères peuvent être combinés. Le cache expire alors lorsqu'au moins un critère n'est pas rempli.

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

Invalidation à l'aide de balises

Les balises sont un outil d'invalidation très utile. Nous pouvons attribuer une liste de balises, qui sont des chaînes de caractères arbitraires, à chaque élément stocké dans le cache. Par exemple, supposons que nous ayons une page HTML avec un article et des commentaires, que nous voulons mettre en cache. Nous spécifions donc des balises lors de l'enregistrement dans le cache :

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

Maintenant, passons à l'administration. Ici, nous avons un formulaire pour l'édition des articles. En même temps que la sauvegarde de l'article dans une base de données, nous appelons la commande clean(), qui supprimera les articles mis en cache par étiquette :

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

De même, au lieu d'ajouter un nouveau commentaire (ou de modifier un commentaire), nous n'oublierons pas d'invalider la balise correspondante :

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

Qu'avons-nous obtenu ? Que notre cache HTML sera invalidé (supprimé) à chaque fois que l'article ou les commentaires changeront. Lorsque vous modifiez un article avec ID = 10, la balise article/10 est forcée d'être invalidée et la page HTML portant la balise est supprimée du cache. La même chose se produit lorsque vous insérez un nouveau commentaire sous l'article concerné.

Les tags nécessitent Journal.

Invalidation par priorité

Nous pouvons définir la priorité des éléments individuels dans le cache, et il sera possible de les supprimer de manière contrôlée lorsque, par exemple, le cache dépasse une certaine taille :

$dependencies[Cache::Priority] = 50;

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

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

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

Effacer le cache

Le paramètre Cache::All efface tout :

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

Lecture en vrac

Pour la lecture et l'écriture en masse dans le cache, on utilise la méthode bulkLoad(), où l'on passe un tableau de clés et on obtient un tableau de valeurs :

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

La méthode bulkLoad() fonctionne de manière similaire à load() avec le deuxième paramètre de rappel, auquel on passe la clé de l'élément généré :

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

Mise en cache de la sortie

La sortie peut être capturée et mise en cache de manière très élégante :

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

	echo ... // impression de quelques données

	$capture->end(); // sauvegarde de la sortie dans le cache
}

Dans le cas où la sortie est déjà présente dans le cache, la méthode capture() l'imprime et renvoie null, de sorte que la condition ne sera pas exécutée. Sinon, elle commence à mettre en mémoire tampon la sortie et renvoie l'objet $capture à l'aide duquel nous sauvegardons finalement les donné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 modèles Latte est très facile, il suffit d'envelopper une partie du modèle avec des balises {cache}...{/cache}. Le cache est automatiquement invalidé lorsque le modèle source est modifié (y compris tout modèle inclus dans les balises {cache} ). Les balises {cache} peuvent être imbriquées, et lorsqu'un bloc imbriqué est invalidé (par exemple, par une balise), le bloc parent est également invalidé.

Dans la balise, il est possible de spécifier les clés auxquelles le cache sera lié (ici la variable $id) et de définir les balises d' expiration et d'invalidation.

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

Tous les paramètres sont facultatifs, il n'est donc pas nécessaire de spécifier l'expiration, les balises ou les clés.

L'utilisation du cache peut également être conditionnée par if – le contenu sera alors mis en cache uniquement si la condition est remplie :

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

Stockages

Un stockage est un objet qui représente l'endroit où les données sont physiquement stockées. Nous pouvons utiliser une base de données, un serveur Memcached, ou le stockage le plus disponible, qui sont les fichiers sur le disque.

Stockage Description
FileStorage: stockage par défaut avec sauvegarde dans des fichiers sur le disque.  
MemcachedStorage utilise le serveur Memcached
MemoryStorage – Les données sont stockées temporairement en mémoire.  
SQLiteStorage – Les données sont stockées dans une base de données SQLite.  
DevNullStorage – Les données ne sont pas stockées – à des fins de test.  

Vous obtenez l'objet de stockage en le passant en utilisant l'injection de dépendance avec le type Nette\Caching\Storage. Par défaut, Nette fournit un objet FileStorage qui stocke les données dans un sous-dossier cache dans le répertoire des fichiers temporaires.

Vous pouvez modifier le stockage dans la configuration :

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

FileStorage

Ecrit le cache dans des fichiers sur le disque. Le stockage Nette\Caching\Storages\FileStorage est très bien optimisé pour les performances et assure surtout une atomicité totale des opérations. Qu'est-ce que cela signifie ? Que lors de l'utilisation du cache, il ne peut pas arriver que l'on lise un fichier qui n'a pas encore été complètement écrit par un autre thread, ou que quelqu'un le supprime “sous vos mains”. L'utilisation du cache est donc totalement sûre.

Ce stockage possède également une importante fonctionnalité intégrée qui empêche une augmentation extrême de l'utilisation du CPU lorsque le cache est effacé ou froid (c'est-à-dire non créé). Il s'agit de la prévention de la ruée vers le cache. Il arrive qu'à un moment donné, plusieurs requêtes concurrentes souhaitent obtenir la même chose du cache (par exemple, le résultat d'une requête SQL coûteuse) et, comme il n'est pas mis en cache, tous les processus commencent à exécuter la même requête SQL. La charge du processeur est multipliée et il peut même arriver qu'aucun thread ne puisse répondre dans le délai imparti, que le cache ne soit pas créé et que l'application plante. Heureusement, le cache de Nette fonctionne de telle manière que lorsqu'il y a plusieurs demandes simultanées pour un même élément, il est généré uniquement par le premier thread, les autres attendent et utilisent ensuite le résultat généré.

Exemple de création d'un FileStorage :

// le stockage sera le répertoire '/path/to/temp' sur le disque.
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');

MemcachedStorage

Le serveur Memcached est un système de stockage distribué haute performance dont l'adaptateur est Nette\Caching\Storages\MemcachedStorage. Dans la configuration, spécifiez l'adresse IP et le port s'ils diffèrent du standard 11211.

Requiert l'extension PHP memcached.

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

MemoryStorage

Nette\Caching\Storages\MemoryStorage est un stockage qui enregistre les données dans un tableau PHP et qui est donc perdu lorsque la requête est terminée.

SQLiteStorage

La base de données SQLite et l'adaptateur Nette\Caching\Storages\SQLiteStorage offrent un moyen de mettre en cache un seul fichier sur le disque. La configuration spécifiera le chemin vers ce fichier.

Nécessite les extensions PHP pdo et pdo_sqlite.

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

DevNullStorage

Une implémentation spéciale du stockage est Nette\Caching\Storages\DevNullStorage, qui ne stocke pas du tout de données. Elle convient donc aux tests si l'on veut éliminer l'effet du cache.

Utilisation du cache dans le code

Lorsque vous utilisez la mise en cache dans le code, vous avez deux façons de procéder. La première consiste à obtenir l'objet de stockage en le passant à l'aide de l'injection de dépendances, puis à créer un 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 deuxième façon est que vous obtenez l'objet de stockage Cache:

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

L'objet Cache est alors créé directement dans la configuration comme suit :

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

Journal

Nette stocke les tags et les priorités dans un journal. Par défaut, SQLite et le fichier journal.s3db sont utilisés pour cela, et les extensions PHP pdo et pdo_sqlite sont nécessaires.

Vous pouvez changer le 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 journal
cache.storage Nette\Caching\Storage repository
version: 3.x