Quanto più un'applicazione genera traffico, tanto più consumerà risorse ad avremo la necessità di mantenerla performante. In Rete si trovano tantissimi sistemi di cache che utilizzano altrettante tecniche differenti per evitare di rigenerare il contenuto dinamico di una pagina ad ogni caricamento. Symfony si differenzia da tutti gli altri però perché, anziché implementare un nuovo sistema di cache, si limita a sfruttare le potenzialità della cache già fornita dal protocollo HTTP.
Quando avviene una richiesta HTTP, gli headers inviati vengono analizzati da tre tipi di cache:
Tipologia | Descrizione |
---|---|
Cache del browser | Si occupa principalmente di memorizzare localmente i contenuti in maniera da fornirli più velocemente alla pressione del tasto indietro. |
Proxy cache | Si può trovare nelle grandi aziende oppure implementata dagli ISP, mantiene salvate le pagine caricate alla prima richiesta e, ad una nuova richiesta da un client all'interno della stessa rete, fornisce la pagina cachata. |
Reverse proxy | Simile al proxy cache ma installata sul server dell'applicazione. |
Vediamo insieme come abilitare la cache nella nostra applicazione sfruttando appunto il Reverse Proxy.
Reverse Proxy
In Symfony la cache è un layer che si interpone tra l'applicazione e il browser (o comunque qualsiasi altro client che effettua la richiesta). Durante queste operazioni la cache si occuperà di "salvare" tutto quello che riterrà opportuno cachare e, ad una nuova richiesta, fornirne una versione già salvata senza passare per l'applicazione.
Se apriamo il file web/app.php
notiamo la presenza di alcune righe commentate:
require_once __DIR__.'/../app/AppKernel.php';
//require_once __DIR__.'/../app/AppCache.php';
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
//$kernel = new AppCache($kernel);
// When using the HttpCache, you need to call the method in your front controller instead of relying on the configuration parameter
//Request::enableHttpMethodParameterOverride();
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
Sarà sufficiente decommentare tali righe per abilitare un Reverse Proxy nella nostra applicazione. Ora prestiamo attenzione alla riga:
$kernel = new AppCache($kernel);
È qui che passiamo la nostra applicazione come parametro dell'oggetto AppCache
che, appunto, è la libreria che si occupa di gestire le richieste, interpretarle e fornire una risposta cachata quando possibile.
Vediamo ora come Symfony interpreta le richieste e valuta se fornire o meno una versione cachata della pagina.
Quando avviene una richiesta, HTTP utilizza diversi headers per specificare la cache (Cache-Control
, Expires
, ETag
, Last-Modified
); quello che vedremo subito nel dettaglio è Cache-Control
che può contenere diverse informazioni tra cui la necessità o meno di utilizzare la cache.
Un esempio di valore per il Cache-Control è:
cache-control: private, max-age=0, must-revalidate
Un contenuto del genere indica all'applicazione che la richiesta non può essere cachata. Il primo parametro (private
) stabilisce che la richiesta è privata e quindi riferita al singolo utente che la sta effettuando. Non deve, perciò, essere cachato. Gli altri due parametri indicano nell'ordine la durata massima del contenuto cachato (in questo caso appunto "0") e se il client ha la necessità o meno (in questo caso sì) di richiedere al server se il contenuto è stato modificato.
Symfony, all'interno dell'oggetto Response
fornisce dei comodissimi metodi per gestire il Cache-Control:
$response = new Response();
$response->setPublic();
$response->setPrivate();
$response->setMaxAge(0);
$response->headers->addCacheControlDirective('must-revalidate', true);
L'esempio di codice appena visto definisce i tre parametri del Cache-Control. Chiaramente questo codice andrà inserito all'interno del controller prima di restituire la Response
.
Un altro metodo per stabilire la scadenza della risposta è utilizzare il metodo
$response->setExpires(60);
Esso setta il valore Expires
dell'header. Tale soluzione però presenta alcune limitazioni date ad esempio dalla necessità di utilizzare l'ora del server. È consigliabile quindi adottare il metodo visto in precedenza.
Un altro header utilizzato per la cache è ETag
. In sostanza l'header è una stringa che univocamente contraddistingue una risorsa nel server. In questo caso si può verificare che la stringa della request
corrisponda a quella della response
e, in quel caso, viene fornita la risorsa cachata. Un esempio di codice:
$response = $this->render('mytemplate.html.twig');
$response->setETag(md5($response->getContent());
$response->setPublic();
$response->isNotModified($request);
L'ultimo header è Last-Modified
che indica la data e l'ora in cui la risorse è stata modificata per l'ultima volta. Nel caso si stia visualizzando la pagina di un libro creato nelle lezioni precedenti, si potrebbe impostare la data di ultima modifica nel database del libro. L'header può essere impostato con i seguenti metodi:
$date = new \DateTime($book->getUpdatedAt());
$response->setLastModified($date);
$response->setPublic();
//se non è cambiato il contenuto ritorno quello già cachato
if ($response->isNotModified($request)) {
return $response;
}
//... altrimenti costruisco la risposta e la ritorno
return $this->render(
'AcmeDemoBundle:Book:view.html.twig',
array(
'book' => $book
)
);
Ora che abbiamo visto gli strumenti che abbiamo a disposizione, combiniamoli per implementare un nostro sistema di cache che gestirà la action view
del controller Book
implementato nelle scorse lezioni.
public function viewAction( $id )
{
$book = $this->getDoctrine()
->getRepository('AcmeDemoBundle:Book')
->findOne($id);
if (!$book) {
throw $this->createNotFoundException(
'Nessun libro presente nel database con l\'id '.$id
);
}
$response = new Response();
$response->setETag($book->getETag()); //bisognerà creare un nuovo metodo che, ad esempio, restituisce un md5 del titolo del libro oppure del contenuto del libro in modo da avere un contenuto univoco
$response->setLastModified($book->getUpdatedAt()); //bisognerà creare un nuovo campo nel database che conterrà la data di modifica, il metodo dovrà restituire un oggetto \DateTime
$response->setPublic();
//se non è cambiato il contenuto ritorno quello già cachato
if ($response->isNotModified($request)) {
return $response;
}
//... altrimenti costruisco la risposta e la ritorno
return $this->render(
'AcmeDemoBundle:Book:view.html.twig',
array(
'book' => $book
)
);
}
Dando uno sguardo al codice, abbiamo effettuato i seguenti step:
- recuperato con l'entity manager il libro che vogliamo visualizzare;
- impostato il valore di ETag. Come inserito nel commento andremo a creare nell'entity
Book
un metodogetETag()
che restituisce un valore univoco per la risorsa; - impostato la data di ultima modifica;
- definito la risposta come pubblica, altrimenti verrebbe considerata privata e non cachata;
- verificato se la richiesta è stata modificata o meno. Nel primo caso restituisco la response già cachata, altrimenti genero la nuova risposta.
La lezione corrente non può essere ovviamente esaustiva riguardo ai numerosissimi aspetti legati alla gestione delle cache. A questo proposito infatti vi sono ulteriori argomenti da approfondire, tra cui la tecnologia ESI, la cache di Doctrine e Varnish per avere applicazioni ancora più performanti.