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.