Nette Caching
La cache accelera la vostra applicazione conservando i dati che una volta erano costosi da ottenere, così in futuro vi si accede più in fretta. Vedremo:
- come usare la cache
- come cambiare il backend di storage
- come invalidare correttamente la cache
Usare la cache in Nette è molto semplice, eppure copre esigenze di caching sofisticate. È progettata per le prestazioni e per una durabilità del 100%. Comprende adattatori per i backend di storage più diffusi. Supporta l'invalidazione tramite tag, la scadenza temporale, la protezione contro il cache stampede e altro.
Installazione
Il pacchetto si scarica e si installa con Composer:
composer require nette/caching
Uso di base
L'elemento centrale per lavorare con la cache è l'oggetto Nette\Caching\Cache. Ne creiamo un'istanza passando al
costruttore un oggetto di storage. Questo oggetto rappresenta il luogo fisico in cui i dati verranno salvati (database,
Memcached, file su disco ecc.). L'oggetto di storage lo ottenete di solito con la dependency injection chiedendo il tipo
Nette\Caching\Storage. L'essenziale lo imparerete nella sezione sugli storage.
Nella versione 3.0 l'interfaccia aveva ancora il prefisso I, quindi si chiamava
Nette\Caching\IStorage. Inoltre le costanti della classe Cache si scrivevano in maiuscolo, per esempio
Cache::EXPIRE invece di Cache::Expire.
Per gli esempi seguenti supponiamo di avere un alias Cache e un'istanza di storage nella variabile
$storage.
use Nette\Caching\Cache;
$storage = /* ... */; // istanza di Nette\Caching\Storage
La cache è in sostanza un key-value store, cioè leggiamo e scriviamo i dati con delle chiavi, come negli array associativi. Le applicazioni sono composte da più parti indipendenti. Se tutte le parti usassero un unico storage (immaginate un'unica directory su disco), prima o poi si verificherebbero collisioni di chiavi. Il Nette Framework lo risolve dividendo lo spazio dello storage in namespace (concettualmente come delle sottodirectory). Ogni parte dell'applicazione lavora poi nel proprio namespace con un nome univoco e non si verifica alcuna collisione.
Il nome del namespace si indica come secondo argomento del costruttore della classe Cache:
$cache = new Cache($storage, 'Full Html Pages');
Se serve, da un'istanza esistente potete derivare una nuova cache limitata a un sotto-namespace con il metodo
derive():
$subCache = $cache->derive('Images');
Ora possiamo usare l'oggetto $cache per leggere dalla cache e scriverci. A entrambi gli scopi serve il metodo
load(). Il primo argomento è la chiave, il secondo un callback PHP che viene richiamato se la chiave non si trova
nella cache. Il callback genera il valore, lo restituisce e il metodo load() lo mette in cache:
$value = $cache->load($key, function () use ($key) {
$computedValue = /* ... */; // calcolo costoso
return $computedValue;
});
Se il secondo parametro viene omesso ($value = $cache->load($key)), load() restituisce
null se l'elemento non si trova nella cache.
È ottimo che si possano mettere in cache non solo stringhe, ma qualsiasi struttura serializzabile. Lo stesso vale per le chiavi.
Per cancellare un elemento dalla cache serve il metodo remove():
$cache->remove($key);
Un elemento si può salvare in cache anche con il metodo
$cache->save($key, $data, ?array $dependencies = null). In generale però si preferisce l'approccio con
load() mostrato sopra.
Memoizzazione
La memoizzazione consiste nel mettere in cache il risultato della chiamata di una funzione o di un metodo, così che alla chiamata successiva con gli stessi argomenti venga restituito il risultato dalla cache invece di ricalcolarlo.
Metodi e funzioni si possono chiamare in modo memoizzato con call(callable $callback, ...$args):
$result = $cache->call('gethostbyaddr', $ip);
La funzione gethostbyaddr() viene quindi chiamata una sola volta per ogni argomento $ip diverso. Le
chiamate successive con lo stesso $ip restituiranno il valore dalla cache.
Si può anche creare un wrapper memoizzato attorno a un metodo o a una funzione, da chiamare in seguito:
function factorial($num)
{
return /* ... */;
}
$memoizedFactorial = $cache->wrap('factorial');
$result = $memoizedFactorial(5); // la prima volta lo calcola
$result = $memoizedFactorial(5); // la seconda volta lo restituisce dalla cache
Scadenza e invalidazione
Quando si usa la cache bisogna affrontare la questione di quando i dati salvati in precedenza diventano non validi. Il Nette Framework offre meccanismi per limitare la validità dei dati o per cancellarli esplicitamente (nella terminologia del framework si parla di “invalidazione”).
La validità dei dati si imposta al momento del salvataggio, tipicamente con il terzo parametro del metodo save(),
per esempio:
$cache->save($key, $value, [
$cache::Expire => '20 minutes',
]);
In alternativa si può impostare con il parametro $dependencies passato per riferimento al callback nel metodo
load(), per esempio:
$value = $cache->load($key, function (&$dependencies) {
$dependencies[Cache::Expire] = '20 minutes';
return /* ... */;
});
Oppure usando il terzo parametro del metodo load() stesso, per esempio:
$value = $cache->load($key, function () {
return /* ... */;
}, [Cache::Expire => '20 minutes']);
Negli esempi seguenti supporremo la seconda variante, quella che usa la variabile $dependencies dentro il
callback.
Scadenza
La forma più semplice di scadenza è il limite di tempo. Questo mette in cache i dati con una validità di 20 minuti:
// accetta anche il numero di secondi oppure un timestamp UNIX
$dependencies[Cache::Expire] = '20 minutes';
Se volete che il periodo di validità si prolunghi a ogni lettura (scadenza scorrevole), potete ottenerlo così, ma tenete presente che questo aumenta il carico della cache:
$dependencies[Cache::Sliding] = true;
Un'opzione utile è far scadere i dati quando un determinato file, o uno di più file, viene modificato. Torna utile per esempio quando si mettono in cache dati derivati dall'elaborazione di questi file. Usate percorsi assoluti.
$dependencies[Cache::Files] = '/path/to/data.yaml';
// oppure
$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];
Possiamo far scadere un elemento della cache quando scade un altro elemento determinato (o uno di più altri). Torna utile
quando si mettono in cache per esempio un'intera pagina HTML e i suoi frammenti sotto chiavi diverse. Quando un frammento cambia,
tutta la pagina va invalidata. Se i frammenti sono salvati sotto chiavi come frag1 e frag2, usate:
$dependencies[Cache::Items] = ['frag1', 'frag2'];
La scadenza si può governare anche con funzioni o metodi statici personalizzati. Vengono chiamati a ogni lettura per
stabilire se l'elemento è ancora valido. Possiamo per esempio far scadere un elemento ogni volta che cambia la versione di PHP.
Create una funzione che confronta la versione attuale con un parametro e, al salvataggio, aggiungete alle dipendenze un array nel
formato [nome della funzione, ...argomenti]:
function checkPhpVersion($ver): bool
{
return $ver === PHP_VERSION_ID;
}
$dependencies[Cache::Callbacks] = [
['checkPhpVersion', PHP_VERSION_ID] // scade quando checkPhpVersion(...) === false
];
Naturalmente tutti questi criteri si possono combinare. L'elemento della cache scade se almeno un criterio non è più soddisfatto.
$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';
Invalidazione tramite tag
I tag offrono un meccanismo di invalidazione molto utile. A ogni elemento salvato nella cache possiamo assegnare un elenco di tag (stringhe qualsiasi). Supponiamo per esempio di avere una pagina HTML che mostra un articolo e i suoi commenti, che vogliamo mettere in cache. Al salvataggio indichiamo i tag pertinenti:
$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];
Passiamo ora alla sezione di amministrazione. Qui abbiamo un form per modificare gli articoli. Insieme al salvataggio
dell'articolo nel database chiamiamo il metodo clean() per cancellare gli elementi della cache in base al
loro tag:
$cache->clean([
$cache::Tags => ["article/$articleId"],
]);
Allo stesso modo, quando aggiungiamo un nuovo commento (o ne modifichiamo uno), dobbiamo ricordarci di invalidare il tag corrispondente:
$cache->clean([
$cache::Tags => ["comments/$articleId"],
]);
Che cosa abbiamo ottenuto? La nostra cache HTML verrà ora invalidata (cancellata) ogni volta che cambiano l'articolo associato
o i suoi commenti. Quando si modifica l'articolo con ID = 10, viene invalidato il tag article/10 e la pagina HTML
in cache che porta questo tag viene cancellata. Lo stesso accade quando viene aggiunto un nuovo commento sotto l'articolo in
questione.
I tag richiedono un Journal.
Invalidazione per priorità
Ai singoli elementi della cache possiamo assegnare delle priorità. Questo permette una cancellazione controllata, per esempio quando la cache supera un certo limite di dimensione:
$dependencies[Cache::Priority] = 50;
Per cancellare tutti gli elementi con priorità minore o uguale a 100:
$cache->clean([
$cache::Priority => 100,
]);
Le priorità richiedono il cosiddetto Journal.
Svuotare la cache
Il parametro Cache::All cancella tutto:
$cache->clean([
$cache::All => true,
]);
Lettura in blocco
Per leggere e scrivere nella cache in blocco serve il metodo bulkLoad(). Passategli un array di chiavi e
restituisce un array dei valori corrispondenti:
$values = $cache->bulkLoad($keys);
Il metodo bulkLoad() funziona in modo simile a load() e accetta anch'esso un secondo parametro
callback. Questo callback riceve la chiave dell'elemento che si sta generando:
$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
$computedValue = /* ... */; // calcolo costoso
return $computedValue;
});
Al contrario, per scrivere più elementi in una volta serve il metodo bulkSave(), che accetta un array di coppie
chiave => valore ed eventuali dipendenze:
$cache->bulkSave([
$key1 => $value1,
$key2 => $value2,
], [Cache::Expire => '20 minutes']);
Uso con PSR-16
Per usare Nette Cache con l'interfaccia PSR-16 potete sfruttare il PsrCacheAdapter. Permette un'integrazione
fluida tra Nette Cache e qualsiasi codice o libreria che si aspetta un'implementazione di cache compatibile con PSR-16.
$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage);
Ora potete usare $psrCache come una normale cache PSR-16:
$psrCache->set('key', 'value', 3600); // salva il valore per 1 ora
$value = $psrCache->get('key', 'default');
L'adattatore supporta tutti i metodi definiti in PSR-16, compresi getMultiple(), setMultiple() e
deleteMultiple().
Cache dell'output
L'output si può catturare e mettere in cache in modo molto elegante:
if ($capture = $cache->capture($key)) {
// echo ... stampa di alcuni dati
$capture->end(); // salva l'output nella cache
}
Se l'output è già presente nella cache, il metodo capture() lo stampa e restituisce null, quindi il
blocco della condizione if viene saltato. Altrimenti comincia a bufferizzare l'output e restituisce un oggetto
$capture, che usate per salvare infine nella cache i dati catturati con il suo metodo end().
Nella versione 3.0 questo metodo si chiamava $cache->start().
Cache in Latte
La cache nei template Latte è molto semplice. Basta racchiudere la parte di template
che volete mettere in cache tra i tag {cache}...{/cache}. La cache viene invalidata automaticamente ogni volta che
il file sorgente del template cambia (compresi i template inclusi dentro il blocco messo in cache). I tag {cache}
si possono annidare. Quando un blocco annidato viene invalidato (per esempio tramite un tag), viene invalidato anche il blocco
genitore.
Dentro il tag potete indicare le chiavi a cui la voce di cache sarà legata (qui la variabile $id), impostare un
tempo di scadenza e definire i tag di invalidazione.
{cache $id, expire: '20 minutes', tags: [tag1, tag2]}
...
{/cache}
Tutti questi parametri sono facoltativi, quindi non dovete indicare né la scadenza, né i tag, né le chiavi.
L'uso della cache si può anche rendere condizionale con if: il contenuto verrà messo in cache solo se la
condizione è soddisfatta:
{cache $id, if: !$form->isSubmitted()}
{$form}
{/cache}
Storage
Uno storage è un oggetto che rappresenta il luogo fisico in cui i dati vengono salvati. Possiamo usare un database, un server Memcached, oppure lo storage più a portata di mano: i file su disco.
| Storage | Descrizione |
|---|---|
| FileStorage | Storage predefinito, salva la cache in file su disco. |
| MemcachedStorage | Usa un server Memcached per lo storage. |
| MemoryStorage | I dati sono conservati temporaneamente in memoria (si perdono alla fine della richiesta). |
| SQLiteStorage | I dati sono conservati in un file di database SQLite. |
| DevNullStorage | I dati non vengono affatto salvati; utile per i test. |
L'oggetto di storage lo ottenete con la dependency
injection chiedendo il tipo Nette\Caching\Storage. Per impostazione predefinita Nette offre un oggetto
FileStorage che salva i dati nella sottodirectory cache dentro la directory dei file temporanei.
Lo storage predefinito lo potete cambiare nella configurazione:
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
FileStorage
Scrive le voci di cache in file su disco. Lo storage Nette\Caching\Storages\FileStorage è molto ottimizzato per
le prestazioni e, cosa fondamentale, garantisce la piena atomicità delle operazioni. Che cosa significa? Che usando la cache non
può capitare di leggere un file che un altro thread non ha ancora finito di scrivere, o che qualcuno lo cancelli mentre lo state
leggendo. Usare questo storage di cache è quindi del tutto sicuro.
Questo storage comprende anche un'importante funzione integrata che impedisce un'impennata estrema dell'uso della CPU quando la cache viene svuotata o è ancora “fredda” (cioè non ancora creata). Si tratta della prevenzione del cosiddetto cache stampede. Si verifica quando più richieste concorrenti chiedono contemporaneamente lo stesso elemento della cache (per esempio il risultato di una query SQL costosa). Se l'elemento in quel momento non è in cache, tutti questi processi potrebbero cominciare a eseguire la stessa operazione costosa (come la query SQL). Questo moltiplica il carico del server e può perfino capitare che nessun thread riesca a rispondere entro il limite di tempo, che la cache non venga creata e che l'applicazione si blocchi. Per fortuna la cache di Nette se ne occupa: quando ci sono più richieste concorrenti per lo stesso elemento, solo il primo thread lo genera. Gli altri thread aspettano e poi usano il risultato generato dal primo.
Esempio di creazione di un FileStorage:
// lo storage sarà la directory '/path/to/temp' su disco
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');
MemcachedStorage
Il server Memcached è un sistema distribuito di caching di oggetti in memoria ad alte
prestazioni. Il suo adattatore in Nette è Nette\Caching\Storages\MemcachedStorage. Nella configurazione indicate
l'indirizzo IP del server e la porta, se differisce dalla standard 11211.
Richiede l'estensione PHP memcached.
services:
cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5')
MemoryStorage
Nette\Caching\Storages\MemoryStorage è uno storage che tiene i dati in un array PHP. Di conseguenza i dati si
perdono alla fine della richiesta.
SQLiteStorage
Il database SQLite, insieme all'adattatore Nette\Caching\Storages\SQLiteStorage, offre un modo di mettere in cache
i dati in un unico file su disco. La configurazione indica il percorso di questo file di database.
Richiede le estensioni PHP pdo e pdo_sqlite.
services:
cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db')
DevNullStorage
Un'implementazione particolare di storage è Nette\Caching\Storages\DevNullStorage, che non salva affatto i dati.
È quindi adatta agli scopi di test, quando volete eliminare gli effetti della cache.
Usare la cache nel codice
Quando usate la cache nel vostro codice avete due approcci principali. Il primo è ottenere l'oggetto di storage con la dependency injection e poi creare da soli l'oggetto
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 seconda possibilità è farsi passare direttamente l'oggetto Cache:
class ClassTwo
{
public function __construct(
private Nette\Caching\Cache $cache,
) {
}
}
L'oggetto Cache va poi definito nella configurazione, per esempio così:
services:
- ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') )
Journal
Nette conserva le informazioni su tag e priorità in un cosiddetto journal. Per impostazione predefinita a questo scopo si usa
SQLite tramite il file journal.s3db e sono necessarie le estensioni PHP pdo e
pdo_sqlite.
L'implementazione del journal la potete cambiare nella configurazione:
services:
cache.journal: MyJournal
Servizi DI
Al container DI vengono aggiunti questi servizi:
| Nome | Tipo | Descrizione |
|---|---|---|
cache.journal |
Nette\Caching\Storages\Journal | Lo storage del journal della cache |
cache.storage |
Nette\Caching\Storage | Lo storage principale della cache |
Disattivare la cache
Un modo di disattivare la cache nella vostra applicazione è impostare come backend di storage DevNullStorage:
services:
cache.storage: Nette\Caching\Storages\DevNullStorage
Questa impostazione non influisce sulla cache dei template Latte né su quella del container DI, perché queste librerie non
usano i servizi di nette/caching e gestiscono la propria cache in modo indipendente. Inoltre le loro cache di solito non hanno bisogno di
essere disattivate in modalità di sviluppo.
Se state aggiornando a una versione più recente, guardate la pagina aggiornamento.