Creative Commons License
This work is licensed under a
Creative Commons
Attribution-Share Alike 3.0
Unported License.

Master Symfony2 fundamentals

Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).
trainings.sensiolabs.com

Discover the SensioLabs Support

Access to the SensioLabs Competency Center for an exclusive and tailor-made support on Symfony
sensiolabs.com

Cache HTTP

Le applicazioni web sono dinamiche. Non importa quanto efficiente possa essere un'applicazione, ogni richiesta conterrà sempre overhead rispetto a quando si serve un file statico.

Per la maggior parte delle applicazioni, questo non è un problema. Symfony2 è molto veloce e, a meno che non si stia facendo qualcosa di veramente molto pesante, ogni richiesta sarà gestita rapidamente, senza stressare troppo il server.

Man mano che il sito cresce, però, quell'overhead può diventare un problema. Il processo normalmente seguito a ogni richiesta andrebbe fatto una volta sola. Questo è proprio lo scopo che si prefigge la cache.

La cache sulle spalle dei giganti

Il modo più efficace per migliorare le prestazioni di un'applicazione è mettere in cache l'intero output di una pagina e quindi aggirare interamente l'applicazione a ogni richiesta successiva. Ovviamente, questo non è sempre possibile per siti altamente dinamici, oppure sì? In questo capitolo, mostreremo come funziona il sistema di cache di Symfony2 e perché pensiamo che sia il miglior approccio possibile.

Il sistema di cache di Symfony2 è diverso, perché si appoggia sulla semplicità e sulla potenza della cache HTTP, definita nelle specifiche HTTP. Invence di inventare un altro metodo di cache, Symfony2 abbraccia lo standard che definisce la comunicazione di base sul web. Una volta capiti i fondamenti dei modelli di validazione e scadenza della cache HTTP, si sarà in grado di padroneggiare il sistema di cache di Symfony2.

Per poter imparare come funziona la cache in Symfony2, procederemo in quattro passi:

  • Passo 1: Un gateway cache, o reverse proxy, è un livello indipendente che si situa davanti all'applicazione. Il reverse proxy mette in cache le risposte non appena sono restituite dall'applicazione e risponde alle richieste con risposte in cache, prima che arrivino all'applicazione. Symfony2 fornisce il suo reverse proxy, ma se ne può usare uno qualsiasi.
  • Passo 2: Gli header di cache HTTP sono usati per comunicare col gateway cache e con ogni altra cache tra l'applicazione e il client. Symfony2 fornisce impostazioni predefinite appropriate e una potente interfaccia per interagire con gli header di cache.
  • Passo 3: La scadenza e la validazione HTTP sono due modelli usati per determinare se il contenuto in cache è fresco (può essere riusato dalla cache) o vecchio (andrebbe rigenerato dall'applicazione):
  • Passo 4: Gli Edge Side Include (ESI) consentono alla cache HTTP di essere usata per mettere in cache frammenti di pagine (anche frammenti annidati) in modo indipendente. Con ESI, si può anche mettere in cache una pagina intera per 60 minuti, ma una barra laterale interna per soli 5 minuti.

Poiché la cache con HTTP non è esclusiva di Symfony2, esistono già molti articoli a riguardo. Se si è nuovi con la cache HTTP, raccomandiamo caldamente l'articolo di Ryan Tomayko Things Caches Do. Un'altra risorsa importante è il Cache Tutorial di Mark Nottingham.

Cache con gateway cache

Quando si usa la cache con HTTP, la cache è completamente separata dall'applicazione e si trova in mezzo tra applicazione e client che effettua la richiesta.

Il compito della cache è accettare le richieste dal client e passarle all'applicazione. La cache riceverà anche risposte dall'applicazione e le girerà al client. La cache è un "uomo in mezzo" nella comunicazione richiesta-risposta tra il client e l'applicazione.

Lungo la via, la cache memorizzerà ogni risposta ritenuta "cacheable" (vedere Introduzione alla cache HTTP). Se la stessa risorsa viene richiesta nuovamente, la cache invia la risposta in cache al client, ignorando completamente l'applicazione.

Questo tipo di cache è nota come HTTP gateway cache e ne esistono diverse, come Varnish, Squid in modalità reverse proxy e il reverse proxy di Symfony2.

Tipi di cache

Ma il gateway cache non è l'unico tipo di cache. Infatti, gli header HTTP di cache inviati dall'applicazione sono analizzati e interpretati da tre diversi tipi di cache:

  • Cache del browser: Ogni browser ha la sua cache locale, usata principalmente quando si clicca sul pulsante "indietro" per immagini e altre risorse. La cache del browser è una cache privata, perché le risorse in cache non sono condivise con nessun altro.
  • Proxy cache: Un proxy è una cache condivisa, perché molte persone possono stare dietro a un singolo proxy. Solitamente si trova nelle grandi aziende e negli ISP, per ridurre la latenza e il traffico di rete.
  • Gateway cache: Come il proxy, anche questa è una cache condivisa, ma dalla parte del server. Installata dai sistemisti di rete, rende i siti più scalabili, affidabili e performanti.

Tip

Le gateway cache sono a volte chiamate reverse proxy cache, cache surrogate o anche acceleratori HTTP.

Note

I significati di cache privata e condivisa saranno più chiari quando si parlerà di mettere in cache risposte che contengono contenuti specifici per un singolo utente (p.e. informazioni sull'account).

Ogni risposta dall'applicazione probabilmente attraverserà una o più cache dei primi due tipi. Queste cache sono fuori dal nostro controllo, ma seguono le indicazioni di cache HTTP impostate nella risposta.

Il reverse proxy di Symfony2

Symfony2 ha un suo reverse proxy (detto anche gateway cache) scritto in PHP. Abilitandolo, le risposte in cache dall'applicazione inizieranno a essere messe in cache. L'installazione è altrettanto facile. Ogni una applicazione Symfony2 ha la cache già configurata in AppCache, che estende AppKernel. Il kernel della cache è il reverse proxy.

Per abilitare la cache, modificare il codice di un front controller, per usare il kernel della cache:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// web/app.php
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
require_once __DIR__.'/../app/AppCache.php';

use Symfony\Component\HttpFoundation\Request;

$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
// inserisce AppKernel all'interno di AppCache
$kernel = new AppCache($kernel);
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

Il kernel della cache agirà immediatamente da reverse proxy, mettendo in cache le risposte dell'applicazione e restituendole al client.

Tip

Il kernel della cache ha uno speciale metodo getLog(), che restituisce una rappresentazione in stringa di ciò che avviene a livello di cache. Nell'ambiente di sviluppo, lo si può usare per il debug e la verifica della strategia di cache:

1
error_log($kernel->getLog());

L'oggetto AppCache una una configurazione predefinita adeguata, ma può essere regolato tramite un insieme di opzioni impostabili sovrascrivendo il metodo getOptions():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// app/AppCache.php
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;

class AppCache extends HttpCache
{
    protected function getOptions()
    {
        return array(
            'debug'                  => false,
            'default_ttl'            => 0,
            'private_headers'        => array('Authorization', 'Cookie'),
            'allow_reload'           => false,
            'allow_revalidate'       => false,
            'stale_while_revalidate' => 2,
            'stale_if_error'         => 60,
        );
    }
}

Tip

A meno che non sia sovrascritta in getOptions(), l'opzione debug sarà impostata automaticamente al valore di debug di AppKernel circostante.

Ecco una lista delle opzioni principali:

  • default_ttl: Il numero di secondi per cui un elemento in cache va considerato fresco, quando nessuna informazione esplicita sulla freschezza viene fornita in una risposta. Header espliciti Cache-Control o Expires sovrascrivono questo valore (predefinito: 0);
  • private_headers: Insieme di header di richiesta che fanno scattare il comportamento "privato" Cache-Control sulle risposte che non stabiliscono esplicitamente il loro stato di public o private, tramite una direttiva Cache-Control. (predefinito: Authorization e Cookie);
  • allow_reload: Specifica se il client possa forzare un ricaricamento della cache includendo una direttiva Cache-Control "no-cache" nella richiesta. Impostare a true per aderire alla RFC 2616 (predefinito: false);
  • allow_revalidate: Specifica se il client possa forzare una rivalidazione della cache includendo una direttiva Cache-Control "max-age=0" nella richiesta. Impostare a true per aderire alla RFC 2616 (predefinito: false);
  • stale_while_revalidate: Specifica il numero predefinito di secondi (la granularità è il secondo, perché la precisione del TTL della risposta è un secondo) durante il quale la cache può restituire immediatamente una risposta vecchia mentre si rivalida in background (predefinito: 2); questa impostazione è sovrascritta dall'estensione stale-while-revalidate Cache-Control di HTTP (vedere RFC 5861);
  • stale_if_error: Specifica il numero predefinito di secondi (la granularità è il secondo) durante il quale la cache può servire una risposta vecchia quando si incontra un errore (predefinito: 60). Questa impostazione è sovrascritta dall'estensione stale-if-error Cache-Control di HTTP (vedere RFC 5861).

Se debug è true, Symfony2 aggiunge automaticamente un header X-Symfony-Cache alla risposta, con dentro informazioni utili su hit e miss della cache.

Il reverse proxy di Symfony2 è un grande strumento da usare durante lo sviluppo di un sito oppure quando il deploy di un sito è su un host condiviso, dove non si può installare altro che codice PHP. Ma, essendo scritto in PHP, non può essere veloce quando un proxy scritto in C. Per questo si raccomanda caldamente di usare Varnish o Squid sul server di produzione, se possibile. La buona notizia è che il cambio da un proxy a un altro è facile e trasparente, non implicando alcuna modifica al codice dell'applicazione. Si può iniziare semplicemente con il reverse proxy di Symfony2 e aggiornare successivamente a Varnish, quando il traffico aumenta.

Per maggiori informazioni sull'uso di Varnish con Symfony2, vedere la ricetta Usare Varnish.

Note

Le prestazioni del reverse proxy di Symfony2 non dipendono dalla complessità dell'applicazione. Questo perché il kernel dell'applicazione parte solo quando ha una richiesta a cui deve essere rigirato.

Introduzione alla cache HTTP

Per sfruttare i livelli di cache disponibili, un'applicazione deve poter comunicare quale risposta può essere messa in cache e le regole che stabiliscono quando e come tale cache debba essere considerata vecchia. Lo si può fare impostando gli header di cache HTTP nella risposta.

Tip

Si tenga a mente che "HTTP" non è altro che il linguaggio (un semplice linguaggio testuale) usato dai client web (p.e. i browser) e i server web per comunicare tra loro. La cache HTTP è la parte di tale linguaggio che consente a client e server di scambiarsi informazioni riguardo alla cache.

HTTP specifica quattro header di cache per la risposta di cui ci occupiamo:

  • Cache-Control
  • Expires
  • ETag
  • Last-Modified

L'header più importante e versatile è l'header Cache-Control, che in realtà è un insieme di varie informazioni sulla cache.

Note

Ciascun header sarà spiegato in dettaglio nella sezione Scadenza e validazione HTTP.

L'header Cache-Control

L'header Cache-Control è unico, perché non contiene una, ma vari pezzi di informazione sulla possibilità di una risposta di essere messa in cache. Ogni pezzo di informazione è separato da una virgola:

1
2
3
Cache-Control: private, max-age=0, must-revalidate

Cache-Control: max-age=3600, must-revalidate

Symfony fornisce un'astrazione sull'header Cache-Control, per rendere la sua creazione più gestibile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// ...

use Symfony\Component\HttpFoundation\Response;

$response = new Response();

// segna la risposta come pubblica o privata
$response->setPublic();
$response->setPrivate();

// imposta max age privata o condivisa
$response->setMaxAge(600);
$response->setSharedMaxAge(600);

// imposta una direttiva personalizzata Cache-Control
$response->headers->addCacheControlDirective('must-revalidate', true);

Risposte pubbliche e risposte private

Sia la gateway cache che la proxy cache sono considerate cache "condivise", perché il contenuto della cache è condiviso da più di un utente. Se una risposta specifica per un utente venisse per errore inserita in una cache condivisa, potrebbe successivamente essere restituita a diversi altri utenti. Si immagini se delle informazioni su un account venissero messe in cache e poi restituite a ogni utente successivo che richiede la sua pagina dell'account!

Per gestire questa situazione, ogni risposta può essere impostata a pubblica o privata:

  • pubblica: Indica che la risposta può essere messa in cache sia da che private che da cache condivise;
  • privata: Indica che tutta la risposta, o una sua parte, è per un singolo utente e quindi non deve essere messa in una cache condivisa.

Symfony è conservativo e ha come predefinita una risposta privata. Per sfruttare le cache condivise (come il reverse proxy di Symfony2), la risposta deve essere impostata esplicitamente come pubblica.

Metodi sicuri

La cache HTTP funziona solo per metodi HTTP "sicuri" (come GET e HEAD). Essere sicuri vuol dire che lo stato dell'applicazione sul server non cambia mai quando si serve la richiesta (si può, certamente, memorizzare un'informazione sui log, mettere in cache dati, eccetera). Questo ha due conseguenze molto ragionevoli:

  • Non si dovrebbe mai cambiare lo stato dell'applicazione quando si risponde a una richiesta GET o HEAD. Anche se non si usa una gateway cache, la presenza di proxy cache vuol dire che ogni richiesta GET o HEAD potrebbe arrivare al server, ma potrebbe anche non arrivare.
  • Non aspettarsi la cache dei metodi PUT, POST o DELETE. Questi metodi sono fatti per essere usati quando si cambia lo stato dell'applicazione (p.e. si cancella un post di un blog). Metterli in cache impedirebbe ad alcune richieste di arrivare all'applicazione o di modificarla.

Regole e valori predefiniti della cache

HTTP 1.1 consente per impostazione predefinita la cache di tutto, a meno che non ci sia un header esplicito Cache-Control. In pratica, la maggior parte delle cache non fanno nulla quando la richiesta ha un cookie, un header di autorizzazione, usa un metodo non sicuro (PUT, POST, DELETE) o quando la risposta ha un codice di stato di rinvio.

Symfony2 imposta automaticamente un header Cache-Control conservativo, quando nessun header è impostato dallo sviluppatore, seguendo queste regole:

  • Se non è deinito nessun header di cache (Cache-Control, Expires, ETag o Last-Modified), Cache-Control è impostato a no-cache, il che vuol dire che la risposta non sarà messa in cache;
  • Se Cache-Control è vuoto (ma uno degli altri header di cache è presente), il suo valore è impostato a private, must-revalidate;
  • Se invece almeno una direttiva Cache-Control è impostata e nessuna direttiva public o private è stata aggiunta esplicitamente, Symfony2 aggiunge automaticamente la direttiva private (tranne quando è impostato s-maxage).

Scadenza e validazione HTTP

Le specifiche HTTP definiscono due modelli di cache:

  • Con il modello a scadenza, si specifica semplicemente quanto a lungo una risposta debba essere considerata "fresca", includendo un header Cache-Control e/o uno Expires. Le cache che capiscono la scadenza non faranno di nuovo la stessa richiesta finché la versione in cache non raggiunge la sua scadenza e diventa "vecchia".
  • Quando le pagine sono molto dinamiche (cioè quando la loro rappresentazione varia spesso), il modello a validazione è spesso necessario. Con questo modello, la cache memorizza la risposta, ma chiede al serve a ogni richiesta se la risposta in cache sia ancora valida o meno. L'applicazione usa un identificatore univoco per la risposta (l'header Etag) e/o un timestamp (come l'header Last-Modified) per verificare se la pagina sia cambiata da quanto è stata messa in cache.

Lo scopo di entrambi i modelli è quello di non generare mai la stessa risposta due volte, appoggiandosi a una cache per memorizzare e restituire risposte "fresche".

Le specifiche HTTP definiscono un linguaggio semplice, ma potente, in cui client e server possono comunicare. Come sviluppatori web, il modello richiesta-risposta delle specifiche domina il nostro lavoro. Sfortunatamente, il documento delle specifiche, la RFC 2616, può risultare di difficile lettura.

C'è uno sforzo in atto (HTTP Bis) per riscrivere la RFC 2616. Non descrive una nuova versione di HTTP, ma per lo più chiarisce le specifiche HTTP originali. Anche l'organizzazione è migliore, essendo le specifiche separate in sette parti; tutto ciò che riguarda la cache HTTP si trova in due parti dedicate (P4 - Richieste condizionali e P6 - Cache: Browser e cache intermedie).

Come sviluppatori web, dovremmo leggere tutti le specifiche. Possiedono un chiarezza e una potenza, anche dopo oltre dieci anni dalla creazione, inestimabili. Non ci si spaventi dalle apparenze delle specifiche, il contenuto è molto più bello della copertina.

Scadenza

Il modello a scadenza è il più efficiente e il più chiaro dei due modelli di cache e andrebbe usato ogni volta che è possibile. Quando una risposta è messa in cache con una scadenza, la cache memorizzerà la risposta e la restituirà direttamente, senza arrivare all'applicazione, finché non scade.

Il modello a scadenza può essere implementato con l'uso di due header HTTP, quasi identici: Expires o Cache-Control.

Scadenza con l'header Expires

Secondo le specifiche HTTP, "l'header Expires dà la data e l'ora dopo la quale la risposta è considerata vecchia". L'header Expires può essere impostato con il metodo setExpires() di Response. Accetta un'istanza di DateTime come parametro:

1
2
3
4
$date = new DateTime();
$date->modify('+600 seconds');

$response->setExpires($date);

Il risultante header HTTP sarà simile a questo:

1
Expires: Thu, 01 Mar 2011 16:00:00 GMT

Note

Il metodo setExpires() converte automaticamente la data al fuso orario GMT, come richiesto dalle specifiche.

Si noti che, nelle versioni di HTTP precedenti alla 1.1, non era richiesto al server di origine di inviare l'header Date. Di conseguenza, la cache (p.e. il browser) potrebbe aver bisogno di appoggiarsi all'orologio locale per valuare l'header Expires, rendendo il calcolo del ciclo di vita vulnerabile a difformità di ore. L'header Expires soffre di un'altra limitazione: le specifiche stabiliscono che "i server HTTP/1.1 non dovrebbero inviare header Expires oltre un anno nel futuro."

Scadenza con l'header Cache-Control

A causa dei limiti dell'header Expires, la maggior parte delle volte si userà al suo posto l'header Cache-Control. Si ricordi che l'header Cache-Control è usato per specificare molte differenti direttive di cache. Per la scadenza, ci sono due direttive, max-age e s-maxage. La prima è usata da tutte le cache, mentre la seconda viene considerata solo dalla cache condivise:

1
2
3
4
5
6
// Imposta il numero di secondi dopo cui la risposta
// non dovrebbe più essere considerata fresca
$response->setMaxAge(600);

// Come sopra, ma solo per cache condivise
$response->setSharedMaxAge(600);

L'header Cache-Control avrebbe il seguente formato (potrebbe contenere direttive aggiuntive):

1
Cache-Control: max-age=600, s-maxage=600

Validazione

Quando una risorsa ha bisogno di essere aggiornata non appena i dati sottostanti subiscono una modifica, il modello a scadenza non raggiunge lo scopo. Con il modello a scadenza, all'applicazione non sarà chiesto di restituire la risposta aggiornata, finché la cache non diventa vecchia.

Il modello a validazione si occupa di questo problema. Con questo modello, la cache continua a memorizzare risposte. La differenza è che, per ogni richiesta, la cache chiede all'applicazione se la risposta in cache è ancora valida. Se la cache è ancora valida, l'applicazione dovrebbe restituire un codice di stato 304 e nessun contenuto. Questo dice alla cache che è va bene restituire la risposta in cache.

Con questo modello, principalmente si risparmia banda, perché la rappresentazione non è inviata due volte allo stesso client (invece è inviata una risposta 304). Ma se si progetta attentamente l'applicazione, si potrebbe essere in grado di prendere il minimo dei dati necessari per inviare una risposta 304 e risparmiare anche CPU (vedere sotto per un esempio di implementazione).

Tip

Il codice di stato 304 significa "non modificato". È importante, perché questo codice di stato non contiene il vero contenuto richiesto. La risposta è invece un semplice e leggero insieme di istruzioni che dicono alla cache che dovrebbe usare la sua versione memorizzata.

Come per la scadenza, ci sono due diversi header HTTP che possono essere usati per implementare il modello a validazione: ETag e Last-Modified.

Validazione con header ETag

L'header ETag è un header stringa (chiamato "tag entità") che identifica univocamente una rappresentazione della risorsa in questione. È interamente generato e impostato dall'applicazione, quindi si può dire, per esempio, se la risorsa /about che è in cache sia aggiornata con ciò che l'applicazione restituirebbe. Un ETag è come un'impronta digitale ed è usato per confrontare rapidamente se due diverse versioni di una risorsa siano equivalenti. Come le impronte digitali, ogni ETag deve essere univoco tra tutte le rappresentazioni della stessa risorsa.

Vediamo una semplice implementazione, che genera l'ETag come un md5 del contenuto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use Symfony\Component\HttpFoundation\Request;

public function indexAction(Request $request)
{
    $response = $this->render('MyBundle:Main:index.html.twig');
    $response->setETag(md5($response->getContent()));
    $response->setPublic(); // assicurarsi che la risposta sia pubblica
    $response->isNotModified($request);

    return $response;
}

Il metodo isNotModified() confronta l'ETag inviato con la Request con quello impostato nella Response. Se i due combaciano, il metodo imposta automaticamente il codice di stato della Response a 304.

Questo algoritmo è abbastanza semplice e molto generico, ma occorre creare l'intera Response prima di poter calcolare l'ETag, che non è ottimale. In altre parole, fa risparmiare banda, ma non cicli di CPU.

Nella sezione Ottimizzare il codice con la validazione, mostreremo come si possa usare la validazione in modo più intelligente, per determinare la validità di una cache senza dover fare tanto lavoro.

Tip

Symfony2 supporta anche gli ETag deboli, passando true come secondo parametro del metodo setETag().

Validazione col metodo Last-Modified

L'header Last-Modified è la seconda forma di validazione. Secondo le specifiche HTTP, "l'header Last-Modified indica la data e l'ora in cui il server di origine crede che la rappresentazione sia stata modificata l'ultima volta". In altre parole, l'applicazione decide se il contenuto in cache sia stato modificato o meno, in base al fatto se sia stato aggiornato o meno da quando la risposta è stata messa in cache.

Per esempio, si può usare la data di ultimo aggiornamento per tutti gli oggetti necessari per calcolare la rappresentazione della risorsa come valore dell'header Last-Modified:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use Symfony\Component\HttpFoundation\Request;

public function showAction($articleSlug, Request $request)
{
    // ...

    $articleDate = new \DateTime($article->getUpdatedAt());
    $authorDate = new \DateTime($author->getUpdatedAt());

    $date = $authorDate > $articleDate ? $authorDate : $articleDate;

    $response->setLastModified($date);
    // imposta la risposta come pubblica. Altrimenti, è privata come valore predefinito.
    $response->setPublic();

    if ($response->isNotModified($request)) {
        return $response;
    }

    // ... fare qualcosa per popolare la risposta con il contenuto completo

    return $response;
}

Il metodo Response::isNotModified() confronta l'header If-Modified-Since inviato dalla richiesta con l'header Last-Modified impostato nella risposta. Se sono equivalenti, la Response sarà impostata a un codice di stato 304.

Note

L'header della richiesta If-Modified-Since equivale all'header Last-Modified dell'ultima risposta inviata al client per una determinata risorsa. In questo modo client e server comunicano l'uno con l'altro e decidono se la risorsa sia stata aggiornata o meno da quando è stata messa in cache.

Ottimizzare il codice con la validazione

Lo scopo principale di ogni strategia di cache è alleggerire il carico dell'applicazione. In altre parole, meno un'applicazione fa per restituire una risposta 304, meglio è. Il metodo Response::isNotModified() fa esattamente questo, esponendo uno schema semplice ed efficiente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;

public function showAction($articleSlug, Request $request)
{
    // Prende l'informazione minima per calcolare
    // l'ETag o o il valore di Last-Modified
    // (in base alla Request, i dati sono recuperati da una
    // base dati o da una memoria chiave-valore, per esempio)
    $article = ...;

    // crea una Response con un ETag e/o un header Last-Modified
    $response = new Response();
    $response->setETag($article->computeETag());
    $response->setLastModified($article->getPublishedAt());

    // imposta la risposta come pubblica. Altrimenti, è privata come valore predefinito.
    $response->setPublic();

    // Verifica che la Response non sia modificata per la Request data
    if ($response->isNotModified($request)) {
        // restituisce subito la Response 304
        return $response;
    }

    // qui fare qualcosa, come recuperare altri dati
    $comments = ...;

    // o rendere un template con la $response già iniziata
    return $this->render(
        'MyBundle:MyController:article.html.twig',
        array('article' => $article, 'comments' => $comments),
        $response
    );
}

Quando la Response non è stata modificata, isNotModified() imposta automaticamente il codice di stato della risposta a 304, rimuove il contenuto e rimuove alcuni header che non devono essere presenti in una risposta 304 (vedere setNotModified()).

Variare la risposta

Finora abbiamo ipotizzato che ogni URI avesse esattamente una singola rappresentazione della risorsa interessata. Per impostazione predefinita, la cache HTTP usa l'URI della risorsa come chiave. Se due persone richiedono lo stesso URI di una risorsa che si può mettere in cache, la seconda persona riceverà la versione in cache.

A volte questo non basta e diverse versioni dello stesso URI hanno bisogno di stare in cache in base a uno più header di richiesta. Per esempio, se si comprimono le pagine per i client che supportano per la compressione, ogni URI ha due rappresentazioni: una per i client col supporto e l'altra per i client senza supporto. Questo viene determinato dal valore dell'header di richiesta Accept-Encoding.

In questo caso, occorre mettere in cache sia una versione compressa che una non compressa della risposta di un particolare URI e restituirle in base al valore Accept-Encoding della richiesta. Lo si può fare usando l'header di risposta Vary, che è una lista separata da virgole dei diversi header i cui valori causano rappresentazioni diverse della risorsa richiesta:

1
Vary: Accept-Encoding, User-Agent

Tip

Questo particolare header Vary fa mettere in cache versioni diverse di ogni risorsa in base all'URI, al valore di Accept-Encoding e all'header di richiesta User-Agent.

L'oggetto Response offre un'interfaccia pulita per la gestione dell'header Vary:

1
2
3
4
5
// imposta un header Vary
$response->setVary('Accept-Encoding');

// imposta diversi header Vary
$response->setVary(array('Accept-Encoding', 'User-Agent'));

Il metodo setVary() accetta un nome di header o un array di nomi di header per i quali la risposta varia.

Scadenza e validazione

Si può ovviamente usare sia la validazione che la scadenza nella stessa Response. Poiché la scadenza vince sulla validazione, si può beneficiare dei vantaggi di entrambe. In altre parole, usando sia la scadenza che la validazione, si può istruire la cache per servire il contenuto in cache, controllando ogni tanto (la scadenza) per verificare che il contenuto sia ancora valido.

Altri metodi della risposta

La classe Response fornisce molti altri metodi per la cache. Ecco alcuni dei più utili:

1
2
3
4
5
// Segna la risposta come vecchia
$response->expire();

// Forza la risposta a restituire un 304 senza contenuti
$response->setNotModified();

Inoltre, la maggior parte degli header HTTP relativi alla cache può essere impostata tramite il singolo metodo setCache():

1
2
3
4
5
6
7
8
9
// Imposta le opzioni della cache in una sola chiamata
$response->setCache(array(
    'etag'          => $etag,
    'last_modified' => $date,
    'max_age'       => 10,
    's_maxage'      => 10,
    'public'        => true,
    // 'private'    => true,
));

Usare Edge Side Includes

Le gateway cache sono un grande modo per rendere un sito più prestante. Ma hanno una limitazione: possono mettere in cache solo pagine intere. Se non si possono mettere in cache pagine intere o se le pagine hanno più parti dinamiche, non vanno bene. Fortunatamente, Symfony2 fornisce una soluzione a questi casi, basata su una tecnologia chiamata ESI, o Edge Side Includes. Akamaï ha scritto le specifiche quasi dieci anni fa, consentendo a determinate parti di una pagina di avere differenti strategie di cache rispetto alla pagina principale.

Le specifiche ESI descrivono dei tag che si possono inserire nelle proprie pagine, per comunicare col gateway cache. L'unico tag implementato in Symfony2 è include, poiché è l'unico utile nel contesto di Akamaï:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!DOCTYPE html>
<html>
    <body>
        <!-- ... del contenuto -->

        <!-- Inserisce qui il contenuto di un'altra pagina -->
        <esi:include src="http://..." />

        <!-- ... dell'altro contenuto -->
    </body>
</html>

Note

Si noti nell'esempio che ogni tag ESI ha un URL pienamente qualificato. Un tag ESI rappresenta un frammento di pagina che può essere recuperato tramite l'URL fornito.

Quando gestisce una richiesta, il gateway cache recupera l'intera pagina dalla sua cache oppure la richiede dall'applicazione di backend. Se la risposta contiene uno o più tag ESI, questi vengono processati nello stesso modo. In altre parole, la gateway cache o recupera il frammento della pagina inclusa dalla sua cache oppure richiede il frammento di pagina all'applicazione di backend. Quando tutti i tag ESI sono stati risolti, il gateway cache li fonde nella pagina principale e invia il contenuto finale al client.

Tutto questo avviene in modo trasparente a livello di gateway cache (quindi fuori dall'applicazione). Come vedremo, se si scegli di avvalersi dei tag ESI, Symfony2 rende quasi senza sforzo il processo di inclusione.

Usare ESI in Symfony2

Per usare ESI, assicurarsi prima di tutto di abilitarlo nella configurazione dell'applicazione:

  • YAML
    1
    2
    3
    4
    # app/config/config.yml
    framework:
        # ...
        esi: { enabled: true }
    
  • XML
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/symfony"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:framework="http://symfony.com/schema/dic/symfony"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                            http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config ...>
            <!-- ... -->
            <framework:esi enabled="true" />
        </framework:config>
    
    </container>
  • PHP
    1
    2
    3
    4
    5
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        // ...
        'esi'    => array('enabled' => true),
    ));
    

Supponiamo ora di avere una pagina relativamente statica, tranne per un elenco di news in fondo al contenuto. Con ESI, si può mettere in cache l'elenco di news indipendentemente dal resto della pagina.

1
2
3
4
5
6
7
8
public function indexAction()
{
    $response = $this->render('MyBundle:MyController:index.html.twig');
    // imposta il tempo massimo condiviso, il che rende la risposta pubblica
    $response->setSharedMaxAge(600);

    return $response;
}

In questo esempio, abbiamo dato alla cache della pagina intera un tempo di vita di dieci minuti. Successivamente, includiamo l'elenco di news nel template, includendolo in un'azione. Possiamo farlo grazie all'aiutante render (vedere Inserire controllori per maggiori dettagli).

Poiché il contenuto incluso proviene da un'altra pagina (o da un altro controllore), Symfony2 usa l'aiutante render per configurare i tag ESI:

  • Twig
    1
    2
    3
    4
    5
    {# si può fare riferimento a un controllore #}
    {{ render_esi(controller('...:news', { 'max': 5 })) }}
    
    {# ... o a un URL #}
    {{ render_esi(url('latest_news', { 'max': 5 })) }}
    
  • PHP
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <?php echo $view['actions']->render(
        new ControllerReference('...:news', array('max' => 5)),
        array('renderer' => 'esi'))
    ?>
    
    <?php echo $view['actions']->render(
        $view['router']->generate('latest_news', array('max' => 5), true),
        array('renderer' => 'esi'),
    ) ?>
    

Usando l'opzione esi``(che usa a sua volta la funzoine Twig ``render_esi), si dice a Symfony2 che l'azione va resa come tag ESI. Ci si potrebbe chiedere perché voler usare un aiutante invece di scrivere direttamente il tag ESI. Il motivo è che un aiutante fa funzionare l'applicazione anche se non ci sono gateway per la cache installati.

Quando si usa la funzione render predefinita (o si usa l'opzione inline), Symfony2 fonde il contenuto della pagina inclusa in quello principale, prima di inviare la risposta al client. Se invece si usa l'opzione esi (che richiama render_esi) e se Symfony2 capisce che sta parlando a un gateway per la cache che supporti ESI, genera un tag ESI. Ma se non c'è alcun gateway per la cache o se ce n'è uno che non supporta ESI, Symfony2 fonderà il contenuto della pagina inclusa in quello principale, come se fosse state usata render.

Note

Symfony2 individua se una gateway cache supporta ESI tramite un'altra specifica di Akamaï, che è supportata nativamente dal reverse proxy di Symfony2.

L'azione inclusa ora può specificare le sue regole di cache, indipendentemente dalla pagina principale.

1
2
3
4
5
6
public function newsAction($max)
{
    // ...

    $response->setSharedMaxAge(60);
}

Con ESI, la cache dell'intera pagina sarà valida per 600 secondi, mentre il componente delle news avrà una cache che dura per soli 60 secondi.

Quando si fa riferimento a un controllore, il tag ESI dovrebbe far riferimento all'azione inclusa con un URL accessibile, in modo che il gateway della cache possa recuperarla indipendentemente dal resto della pagina. Symfony2 si occupa di generare un URL univoco per ogni riferimento a controllori ed è in grado di puntare correttamente le rotte, grazie all'ascoltatore FragmentListener, che va abilitato nella configurazione:

  • YAML
    1
    2
    3
    4
    # app/config/config.yml
    framework:
        # ...
        fragments: { path: /_fragment }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <!-- app/config/config.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:doctrine="http://symfony.com/schema/dic/framework"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                            http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">
    
        <framework:config>
            <framework:fragments path="/_fragment" />
        </framework:config>
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        // ...
        'fragments' => array('path' => '/_fragment'),
    ));
    

Un grosso vantaggio di questa strategia di cache è che si può rendere l'applicazione tanto dinamica quanto necessario e, allo stesso tempo, mantenere gli accessi al minimo.

Tip

L'ascoltatore risponde solo agli indirizzi IP locali o ai proxy fidati.

Note

Una volta iniziato a usare ESI, si ricordi di usare sempre la direttiva s-maxage al posto di max-age. Poiché il browser riceve la risorsa aggregata, non ha visibilità sui sotto-componenti, quindi obbedirà alla direttiva max-age e metterà in cache l'intera pagina. E questo non è quello che vogliamo.

L'aiutante render supporta due utili opzioni:

  • alt: usato come attributo alt nel tag ESI, che consente di specificare un URL alternativo da usare, nel caso in cui src non venga trovato;
  • ignore_errors: se impostato a true, un attributo onerror sarà aggiunto a ESI con il valore di continue, a indicare che, in caso di fallimento, la gateway cache semplicemente rimuoverà il tag ESI senza produrre errori.

Invalidazione della cache

"Ci sono solo due cose difficili in informatica: invalidazione della cache e nomi delle cose." Phil Karlton

Non si dovrebbe mai aver bisogno di invalidare i dati in cache, perché dell'invalidazione si occupano già nativamente i modelli di cache HTTP. Se si usa la validazione, non si avrà mai bisogno di invalidare nulla, per definizione; se si usa la scadenza e si ha l'esigenza di invalidare una risorsa, vuol dire che si è impostata una data di scadenza troppo in là nel futuro.

Note

Essendo l'invalidazione un argomento specifico di ogni reverse proxy, se non ci si preoccupa dell'invalidazione, si può cambiare reverse proxy senza cambiare alcuna parte del codice dell'applicazione.

In realtà, ogni reverse proxy fornisce dei modi per pulire i dati in cache, ma andrebbero evitati, per quanto possibile. Il modo più standard è pulire la cache per un dato URL richiedendolo con il metodo speciale HTTP PURGE.

Ecco come si può configurare il reverse proxy di Symfony2 per supportare il metodo HTTP PURGE:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// app/AppCache.php

// ...
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class AppCache extends HttpCache
{
    protected function invalidate(Request $request, $catch = false)
    {
        if ('PURGE' !== $request->getMethod()) {
            return parent::invalidate($request, $catch);
        }

        $response = new Response();
        if (!$this->getStore()->purge($request->getUri())) {
            $response->setStatusCode(Response::HTTP_NOT_FOUND, 'Not purged');
        } else {
            $response->setStatusCode(Response::HTTP_OK, 'Purged');
        }

        return $response;
    }
}

New in version 2.4: Il supporto per le costanti dei codici di stato HTTP è stato aggiunto in Symfony 2.4.

Caution

Occorre proteggere in qualche modo il metodo HTTP PURGE, per evitare che qualcuno pulisca casualmente i dati in cache.

Imparare di più con le ricette