Cache
Le cache accélère votre application en stockant les données coûteuses à obtenir pour une utilisation ultérieure. Nous allons vous montrer :
- comment utiliser le cache
- comment changer le stockage
- comment invalider correctement le cache
L'utilisation du cache est très facile dans Nette, tout en couvrant des besoins très avancés. Il est conçu pour la performance et une résilience à 100%. Par défaut, vous trouverez des adaptateurs pour les stockages backend les plus courants. Il permet l'invalidation basée sur les tags, l'expiration temporelle, dispose d'une protection contre le cache stampede, etc.
Installation
La bibliothèque peut être téléchargée et installée en utilisant l'outil Composer :
composer require nette/caching
Utilisation de base
Le cœur du travail avec le cache est l'objet Nette\Caching\Cache. Nous créons son instance et passons
au constructeur ce qu'on appelle un stockage. C'est un objet représentant l'endroit où les données seront physiquement
stockées (base de données, Memcached, fichiers sur disque, …). Nous accédons au stockage en le faisant passer via l'injection de dépendances avec le type
Nette\Caching\Storage
. Vous trouverez tout ce qui est essentiel dans la section
Stockages.
Dans la version 3.0, l'interface avait encore le préfixe I
, donc le nom était
Nette\Caching\IStorage
. De plus, les constantes de la classe Cache
étaient écrites en majuscules, donc
par exemple Cache::EXPIRE
au lieu de Cache::Expire
.
Pour les exemples suivants, supposons que nous avons créé un alias Cache
et que la variable
$storage
contient le stockage.
use Nette\Caching\Cache;
$storage = /* ... */; // instance de Nette\Caching\Storage
Le cache est en fait un key-value store, c'est-à-dire que nous lisons et écrivons des données sous des clés, tout comme avec les tableaux associatifs. Les applications sont composées de nombreuses parties indépendantes, et si toutes utilisaient un seul stockage (imaginez un seul répertoire sur le disque), tôt ou tard, une collision de clés se produirait. Le framework Nette résout ce problème en divisant tout l'espace 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 plus se produire.
Nous spécifions le nom de l'espace comme deuxième paramètre du constructeur de la classe Cache :
$cache = new Cache($storage, 'Full Html Pages');
Maintenant, nous pouvons 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 un callback PHP, qui est appelé
lorsque la clé n'est pas trouvée dans le cache. Le callback génère la valeur, la retourne et elle est stockée dans le
cache :
$value = $cache->load($key, function () use ($key) {
$computedValue = /* ... */; // calcul coûteux
return $computedValue;
});
Si le deuxième paramètre n'est pas fourni $value = $cache->load($key)
, null
est retourné si
l'élément n'est pas dans le cache.
Ce qui est génial, c'est que vous pouvez stocker n'importe quelle structure sérialisable dans le cache, pas seulement des chaînes de caractères. Et la même chose s'applique même aux clés.
Nous supprimons un élément du cache en utilisant la méthode remove()
:
$cache->remove($key);
Vous pouvez également enregistrer un élément dans le cache en utilisant la méthode
$cache->save($key, $value, array $dependencies = [])
. Cependant, la méthode préférée est celle mentionnée
ci-dessus en utilisant load()
.
Memoization
La mémoïsation signifie la mise en cache du résultat d'un appel de fonction ou de méthode afin que vous puissiez l'utiliser la prochaine fois sans recalculer la même chose encore et encore.
Les méthodes et les fonctions peuvent être appelées de manière mémoïsée en utilisant
call(callable $callback, ...$args)
:
$result = $cache->call('gethostbyaddr', $ip);
La fonction gethostbyaddr()
ne sera appelée qu'une seule fois pour chaque paramètre $ip
, et la
prochaine fois, la valeur sera retournée depuis le cache.
Il est également possible de créer un wrapper mémoïsé autour d'une méthode ou d'une fonction, qui peut être appelé plus tard :
function factorial($num)
{
return /* ... */;
}
$memoizedFactorial = $cache->wrap('factorial');
$result = $memoizedFactorial(5); // calcule la première fois
$result = $memoizedFactorial(5); // la deuxième fois depuis le cache
Expiration & Invalidation
Lors de la mise en cache, il est nécessaire de résoudre la question de savoir quand les données précédemment stockées deviennent invalides. Le framework Nette propose un mécanisme pour limiter la validité des données ou les supprimer de manière contrôlée (dans la terminologie du framework, “invalider”).
La validité des données est définie au moment de l'enregistrement à l'aide du troisième paramètre de la méthode
save()
, par exemple :
$cache->save($key, $value, [
$cache::Expire => '20 minutes',
]);
Ou en utilisant le paramètre $dependencies
passé par référence au callback de la méthode load()
,
par exemple :
$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 de la variable
$dependencies
.
Expiration
L'expiration la plus simple est une limite de temps. Voici comment nous mettons en cache les données avec une validité de 20 minutes :
// accepte également le nombre de secondes ou un timestamp UNIX
$dependencies[Cache::Expire] = '20 minutes';
Si nous voulions prolonger la période de validité à chaque lecture, cela peut être réalisé comme suit, mais attention, la surcharge du cache augmentera :
$dependencies[Cache::Sliding] = true;
Une option pratique est de laisser les données expirer lorsqu'un fichier ou l'un des multiples fichiers change. Cela peut être utilisé, par exemple, lors de la mise en cache de données résultant du traitement 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'];
Nous pouvons laisser un élément du cache expirer lorsqu'un autre élément (ou l'un de plusieurs autres) expire. Cela peut
être utilisé lorsque nous mettons en cache, par exemple, une page HTML entière et ses fragments sous d'autres clés. Dès qu'un
fragment change, toute la page est invalidée. Si nous avons des fragments stockés sous les clés frag1
et
frag2
, par exemple, nous utilisons :
$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
à chaque lecture si l'élément est toujours valide. De cette façon, nous pouvons laisser un élément expirer chaque fois que
la version de PHP change. Nous créons une fonction qui compare la version actuelle avec un paramètre, et lors de
l'enregistrement, nous ajoutons un tableau de la forme [nom de la fonction, ...arguments]
aux dépendances :
function checkPhpVersion($ver): bool
{
return $ver === PHP_VERSION_ID;
}
$dependencies[Cache::Callbacks] = [
['checkPhpVersion', PHP_VERSION_ID] // expire quand checkPhpVersion(...) === false
];
Bien sûr, tous les critères peuvent être combinés. Le cache expirera alors si au moins un critère n'est pas rempli.
$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';
Invalidation par tags
Les tags sont un outil d'invalidation très utile. Nous pouvons assigner une liste de tags, qui sont des chaînes arbitraires, à chaque élément du cache. Par exemple, supposons que nous ayons une page HTML avec un article et des commentaires que nous allons mettre en cache. Lors de l'enregistrement, nous spécifions les tags :
$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];
Passons à l'administration. Ici, nous trouvons un formulaire pour éditer l'article. En même temps que l'enregistrement de
l'article dans la base de données, nous appelons la commande clean()
, qui supprime les éléments du cache par
tag :
$cache->clean([
$cache::Tags => ["article/$articleId"],
]);
De même, à l'endroit où un nouveau commentaire est ajouté (ou un commentaire est édité), nous n'oublions pas d'invalider le tag correspondant :
$cache->clean([
$cache::Tags => ["comments/$articleId"],
]);
Qu'avons-nous accompli ? Que notre cache HTML sera invalidé (supprimé) chaque fois que l'article ou les commentaires
changent. Lorsqu'un article avec l'ID = 10 est édité, le tag article/10
est invalidé de force, et la page HTML
portant ce tag est supprimée du cache. La même chose se produit lors de l'insertion d'un nouveau commentaire sous l'article
correspondant.
Les tags nécessitent ce qu'on appelle un Journal.
Invalidation par priorité
Nous pouvons définir une priorité pour les éléments individuels dans le cache, qui peut être utilisée pour les supprimer lorsque, par exemple, le cache dépasse une certaine taille :
$dependencies[Cache::Priority] = 50;
Nous supprimons tous les éléments avec une priorité égale ou inférieure à 100 :
$cache->clean([
$cache::Priority => 100,
]);
Les priorités nécessitent ce qu'on appelle un Journal.
Suppression du cache
Le paramètre Cache::All
supprime tout :
$cache->clean([
$cache::All => true,
]);
Lecture en masse
Pour lire et écrire en masse dans le cache, utilisez la méthode bulkLoad()
, à laquelle nous passons un tableau
de clés et obtenons un tableau de valeurs :
$values = $cache->bulkLoad($keys);
La méthode bulkLoad()
fonctionne de manière similaire à load()
avec le deuxième paramètre
callback, auquel la clé de l'élément généré est passée :
$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
$computedValue = /* ... */; // calcul coûteux
return $computedValue;
});
Utilisation avec PSR-16
Pour utiliser Nette Cache avec l'interface PSR-16, vous pouvez utiliser l'adaptateur PsrCacheAdapter
. Il permet
une intégration transparente entre Nette Cache et tout code ou bibliothèque qui attend un cache compatible PSR-16.
$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);
Vous pouvez maintenant utiliser $psrCache
comme un cache PSR-16 :
$psrCache->set('key', 'value', 3600); // stocke la valeur pendant 1 heure
$value = $psrCache->get('key', 'default');
L'adaptateur prend en charge toutes les méthodes définies dans PSR-16, y compris getMultiple()
,
setMultiple()
et deleteMultiple()
.
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 ... // affichage des données
$capture->end(); // enregistre la sortie dans le cache
}
Si la sortie est déjà stockée dans le cache, la méthode capture()
l'affiche et renvoie null
, donc
la condition n'est pas exécutée. Sinon, elle commence à capturer la sortie et renvoie l'objet $capture
, que nous
utilisons pour finalement enregistrer les données affichées dans le cache.
Dans la version 3.0, la 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'envelopper
une partie du template avec les balises {cache}...{/cache}
. Le cache est automatiquement invalidé lorsque le
template source change (y compris les templates inclus dans le bloc de cache). Les balises {cache}
peuvent être
imbriquées, et lorsqu'un bloc imbriqué est invalidé (par exemple, par un tag), 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 l'expiration et les tags pour l'invalidation.
{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
...
{/cache}
Tous les éléments sont facultatifs, nous n'avons donc pas besoin de spécifier l'expiration, les tags ou même les clés.
L'utilisation du cache peut également être conditionnée à l'aide de if
– le contenu ne sera alors 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 où les données sont physiquement stockées. Nous pouvons utiliser une base de données, un serveur Memcached ou le stockage le plus accessible, qui sont des fichiers sur disque.
Stockage | Description |
---|---|
FileStorage | stockage par défaut avec enregistrement dans des fichiers sur disque |
MemcachedStorage | utilise un serveur Memcached |
MemoryStorage | les données sont 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, adapté aux tests |
Vous accédez à l'objet de stockage en le faisant passer via l'injection de dépendances avec le type
Nette\Caching\Storage
. Nette fournit par défaut l'objet FileStorage, qui stocke les données dans le
sous-répertoire cache
du répertoire des fichiers temporaires.
Vous pouvez modifier le stockage dans la configuration :
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
FileStorage
Écrit le cache dans des fichiers sur disque. Le stockage Nette\Caching\Storages\FileStorage
est très bien
optimisé pour les performances et garantit surtout une atomicité complète des opérations. Qu'est-ce que cela signifie ? Que
lors de l'utilisation du cache, il ne peut pas arriver que nous lisions un fichier qui n'a pas encore été complètement écrit
par un autre thread, ou qu'il soit supprimé “sous nos yeux”. L'utilisation du cache est donc totalement sûre.
Ce stockage dispose également d'une fonction intégrée importante qui empêche une augmentation extrême de l'utilisation du processeur lorsque le cache est supprimé ou n'est pas encore chaud (c'est-à-dire créé). C'est une prévention contre le cache stampede. Il arrive qu'un grand nombre de requêtes simultanées arrivent en même temps, demandant la même chose au cache (par exemple, le résultat d'une requête SQL coûteuse), et comme il n'est pas dans le cache, tous les processus commencent à exécuter la même requête SQL. La charge se multiplie ainsi et il peut même arriver qu'aucun thread ne parvienne à 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 lors de plusieurs requêtes simultanées pour un même élément, seul le premier thread le génère, les autres attendent et utilisent ensuite le résultat généré.
Exemple de création de 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é en mémoire très performant,
dont l'adaptateur est Nette\Caching\Storages\MemcachedStorage
. Dans la configuration, nous spécifions l'adresse IP
et le port, s'ils diffèrent du port standard 11211.
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 enregistre les données dans un tableau PHP, et donc
elles sont perdues à la fin de la requête.
SQLiteStorage
La base de données SQLite et l'adaptateur Nette\Caching\Storages\SQLiteStorage
offrent un moyen de stocker le
cache dans un seul fichier sur disque. Dans la configuration, nous spécifions le chemin d'accès à 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 de stockage est Nette\Caching\Storages\DevNullStorage
, qui en réalité ne stocke
aucune donnée. Il est donc adapté aux tests lorsque nous voulons éliminer l'influence du cache.
Utilisation du cache dans le code
Lors de l'utilisation du cache dans le code, nous avons deux façons de procéder. La première consiste à recevoir le
stockage via l'injection de dépendances et à
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 option est de recevoir directement l'objet Cache
:
class ClassTwo
{
public function __construct(
private Nette\Caching\Cache $cache,
) {
}
}
L'objet Cache
est ensuite créé directement dans la configuration de cette manière :
services:
- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )
Journal
Nette stocke les tags et les priorités dans ce qu'on appelle 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
requises.
Vous pouvez modifier 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 | stockage |
Désactivation du cache
Une des options pour désactiver le cache dans l'application est de définir le stockage sur DevNullStorage :
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
Ce paramètre n'affecte pas la mise en cache des templates dans Latte ou du conteneur DI, car ces bibliothèques n'utilisent pas les services nette/caching et gèrent leur propre cache indépendamment. D'ailleurs, leur cache n'a pas besoin d'être désactivé en mode développeur.