SymfonyWorld Online 2021 Winter Edition December 9 – 10, 2021 100% Online 25 talks and 10 workshops
Caution: You are browsing the legacy symfony 1.x part of this website.

Giorno 21: La Cache

1.4 / Doctrine
Symfony version
1.2
Language ORM

Oggi parleremo della cache. Il framework symfony ha molte strategie di cache da offrire. Per esempio i file di configurazione YAML sono prima convertiti in PHP e poi memorizzati in cache sul filesystem. Abbiamo visto inoltre che i moduli generati dall'admin generator sono salvati in cache per avere migliori prestazioni.

Ma oggi parleremo di un altro tipo di cache: la cache HTML. Per migliorare le prestazioni del sitoi, si possono inserire in cache tutte le pagine HTML o solo parte di esse.

Creare un nuovo ambiente

Di default la funzionalità per la cache dei template è abilitata nel file di configurazione settings.yml per l'ambiente prod, ma non per quello test o dev:

prod:
  .settings:
    cache: true
 
dev:
  .settings:
    cache: false
 
test:
  .settings:
    cache: false

Siccome abbiamo bisogno di testare la funzionalità di cache prima di andare in produzione possiamo attivare la cache per l'ambiente dev o possiamo crearne uno nuovo. Ricordate che un ambiente è definito dal suo nome (una stringa), un front controller associato e opzionalmente un insieme di valori di configurazione specifici.

Per giocare con la cache su Jobeet andremo a creare un ambiente cache simile a quello prod ma con le informazioni dei log e di debug disponibili nell'ambiente dev.

Create il front controller associato con il nuovo ambiente cache copiando il dev front controller web/frontend_dev.php in web/frontend_cache.php:

// web/frontend_cache.php
if (!in_array(@$_SERVER['REMOTE_ADDR'], array('127.0.0.1', '::1')))
{
  die('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');
}
 
require_once(dirname(__FILE__).'/../config/ProjectConfiguration.class.php');
 
$configuration = ProjectConfiguration::getApplicationConfiguration('frontend', 'cache', true);
sfContext::createInstance($configuration)->dispatch();

È tutto quello che occorre fare. Il nuovo ambiente cache è utilizzabile. L'unica differenza è il secondo parametro del metodo getApplicationConfiguration(), che rappresenta il nome dell'ambiente, cache.

Si può testare l'ambiente cache nel browser, richiamando il suo front controller:

http://jobeet.localhost/frontend_cache.php/

note

Lo script del front controller inizia con una parte di codice che si assicura del fatto che la chiamata venga fatta da un indirizzo IP locale. Questa misura di sicurezza protegge il front controller dall'essere chiamato sui server di produzione. Parleremo in modo più approfondito di questo argomento nel tutorial di domani.

Per adesso l'ambiente cache eredita la configurazione dalla configurazione di default. Modificate il file di configurazione settings.yml per aggiungere la configurazione specifica dell'ambiente cache:

# apps/frontend/config/settings.yml
cache:
  .settings:
    error_reporting: <?php echo (E_ALL | E_STRICT)."\n" ?>
    web_debug:       true
    cache:           true
    etag:            false

In queste impostazioni la funzionalità di cache dei template viene attivata con l'impostazione cache mentre la web debug toolbar è stata abilitata con l'impostazione web_debug.

Visto che la configurazione di default salva in cache tutte le impostazioni, occorre cancellarla prima di poter vedere i cambiamenti nel browser:

$ php symfony cc

Ora, se si aggiorna la pagina nel browser, la web debug toolbar dovrebbe essere presente nell'angolo in alto a destra della pagina, come succede per l'ambiente dev.

Configurazione della cache

La cache dei template di symfony può essere configurata attraverso il file cache.yml. La configurazione predefinita per l'applicazione può essere trovata in apps/frontend/config/cache.yml:

default:
  enabled:     false
  with_layout: false
  lifetime:    86400

Di default, visto che tutte le pagine possono contenere contenuti dinamici, la cache è disabilitata in modo globale (enabled: false). Non abbiamo bisogno di cambiare questa impostazione, dato che abiliteremo la cache pagina per pagina.

L'impostazione lifetime definisce la durata della cache lato server in secondi (86400 secondi equivalgono a un giorno).

tip

Si può anche lavorare in modo contrario: abilitare la cache in modo globale e poi disabilitarla su pagine specifiche, che non devono essere inserite in cache. Dipende da quale metodo rappresenta quello meno impegnativo per la propria applicazione.

Cache delle pagine

Siccome l'homepage di Jobeet sarà probabilmente la pagina più visitata del sito, invece che richiedere i dati dal database ogni volta che un utente vi accede, può essere inserita in cache.

Create un file cache.yml per il modulo sfJobeetJob:

# plugins/sfJobeetJob/modules/sfJobeetJob/config/cache.yml
index:
  enabled:     true
  with_layout: true

tip

Il file di configurazione cache.yml ha le stesse proprietà di ogni altro file di configurazione, come view.yml. Significa, per esempio, che si può abilitare la cache per tutte le azioni di un modulo, utilizzando la chiave speciale all.

Aggiornando la pagina nel browser, si vedrà che symfony ha decorato la pagina con un box indicante i contenuti inseriti in cache:

Cache pulita

Il box offre alcune preziose informazioni riguardo la cache per il debugging, come la durata di vita della cache e la sua età.

Se aggiornate ancora la pagina il colore del box cambia da verde a giallo indicando che la pagina è stata recuperata dalla cache:

Cache

Notate inoltre che nessuna richiesta a database è stata effettuate nel secondo caso come mostrato nella web debug toolbar.

tip

Anche se il linguaggio può cambiare da utente a utente la cache continuerà a funzionare visto che la lingua è integrata nell'URL.

Quando una pagina è memorizzabile in cache e se la cache non esiste ancora, symfony salva l'oggetto di risposta nella cache alla fine della richiesta. Per tutte le richieste future symfony invierà la risposta in cache senza richiamare il controllore:

Flusso della cache di una paginaw

Questo ha un grosso impatto sulle prestazioni, come si può constatare personalmente usando strumenti come JMeter.

note

Una richiesta in ingresso con un parametro GET o inviata con un metodo POST, PUT, o DELETE non verrà mai inserita in cache da symfony, indipendentemente dalla configurazione.

La pagina per l'inserimento di un'offerta di lavoro può essere inserita in cache:

# plugins/sfJobeetJob/modules/sfJobeetJob/config/cache.yml
new:
  enabled:     true
 
index:
  enabled:     true
 
all:
  with_layout: true

Dato che le due pagine possono essere inserite in cache con il layout abbiamo creato una sezione all che definisce la configurazione di default per tutte le azioni del modulo sfJobeetJob.

Pulire la Cache

Se si vuole pulire la cache delle pagine, si può usare il task cache:clear:

$ php symfony cc

Il task cache:clear pulisce tutta la cache di symfony salvata nella cartella cache/. Inoltre ha delle opzioni per pulire in modo selettivo alcune parti della cache. Per eliminare solamente la cache dei template per l'ambiente cache utilizzate i parametri --type e --env options:

$ php symfony cc --type=template --env=cache

Invece di pulire la cache ogni volta che si fanno delle modifiche, si può disabilitare la cache aggiungendo ogni query string nell'URL o utilizzando il pulsante "Ignore cache" della web debug toolbar:

Web Debug Toolbar

Cache dell'azione

A volte non si può inserire in cache una pagina intera, ma il template dell'azione può comunque essere inserito. Guardandola diversamente, si può inserire in cache tutto tranne che il layout.

Per l'applicazione Jobeet, non possiamo mettere in cache l'intera pagina, perché c'è la barra della cronologia degli ultimi annunci consultati.

Cambiamo la configurazione della cache per il modulo job_module di conseguenza:

# plugins/sfJobeetJob/modules/sfJobeetJob/config/cache.yml
new:
  enabled:     true
 
index:
  enabled:     true
 
all:
  with_layout: false

Cambiando l'impostazione di with_layout a false, si disabilita la cache del layout.

Pulire la cache:

$ php symfony cc

Ricaricate la pagina nel browser per vederne le differenze:

Cache azione

Anche se il flusso della richiesta è molto simile nel diagramma semplificato, usare la cache senza il layout è molto più dispendioso dal lato delle risorse.

Flusso della cache dell'azione

Cache di Partial e Component

Per i siti web altamente dinamici a volte è impossibile inserire in cache l'intero template dell'azione. Per questi casi occorre di configurare la cache a un livello di granularità maggiore. Fortunatamente, anche partial e component possono essere inseriti in cache.

Cache di partial

Inseriamo in cache il component language creando un file cache.yml per il modulo sfJobeetLanguage:

# plugins/sfJobeetJob/modules/sfJobeetLanguage/config/cache.yml
_language:
  enabled: true

Configurare la cache per un partial o un component è semplice quanto aggiungere una riga con il suo nome. L'opzione with_layout non viene presa in considerazione per questo tipo di cache, visto che non avrebbe alcun senso:

Flusso della cache di Partial e Component

sidebar

Contestuale o no?

Lo stesso component o partial può essere usato in molti template diversi. Ad esempio il partial _list.php dei lavori può essere usato nei moduli sfJobeetJob e sfJobeetCategory. Siccome la resa è sempre la stessa, il partial non dipende dal contesto in cui è usato e la cache è la stessa per tutti i template (la cache resta ovviamente diversa per un diverso insieme di parametri).

Ma a volte l'output di un partial o di un component sono diversi, a seconda dell'azione in cui sono inclusi (pensiamo ad esempio al menù laterale di un blog, che cambia leggermente tra la homepage e le pagine dei post). In questi casi il partial o il component sono contestuali, e la cache deve essere configurata di conseguenza, impostando l'opzione contextual a true:

_sidebar:
  enabled:    true
  contextual: true

Form in cache

Memorizzare la pagina di creazione del lavoro in cache è problematico, perché essa contiene un form. Per capire meglio il problema, si apra la pagina "Post a Job" nel browser, in modo da metterla in cache. Quindi si cancelli il cookie di sessione e si provi di nuovo a inviare il form. Dovrebbe apparire un messaggio di errore, che avverte di un attacco CSRF:

CSRF e Cache

Perché? Siccome abbiamo configurato una chiave CSRF segreta quando abbiamo creato l'applicazione frontend, symfony inserisce un token CSRF in tutti i form. Per proteggersi dagli attacchi CSRF, questo token è unico per ogni utente e per ogni form.

La prima volta che la pagina viene mostrata, il codice HTML del form viene memorizzato nella cache con il token dell'utente attuale. Se successivamente arriva un altro utente, la pagina in cache sarà mostrata, con il token CSRF del primo utente. Quando si invia il form, i token non corrispondono e viene lanciato un errore.

Come possiamo risolvere il problema, visto che sembra legittimo memorizzare il form nella cache? Il form di creazione del lavoro non dipende dall'utente e non modifica nulla per l'utente attuale. In questo caso, non serve nessuna protezione CSRF, quindi possiamo rimuovere il token CSRF:

// plugins/sfJobeetPlugin/lib/form/doctrine/PluginJobeetJobForm.class.php
abstract PluginJobeetJobForm extends BaseJobeetJobForm
{
  public function configure()
  {
    $this->disableLocalCSRFProtection();
  }
}

Dopo aver fatto questa modifica, puliamo la cache e riproviamo lo stesso scenario, per verificare che ora funzioni come ci si aspetta.

La stessa configurazione deve essere applicata al form della lingua, perché è contenuto nel layout e verrà memorizzato in cache. Siccome usiamo sfLanguageForm, invece di creare una nuova classe, per togliere solo il token CSRF, facciamolo dall'azione e dal component del modulo sfJobeetLanguage:

// plugins/sfJobeetJob/modules/sfJobeetLanguage/actions/components.class.php
class sfJobeetLanguageComponents extends sfComponents
{
  public function executeLanguage(sfWebRequest $request)
  {
    $this->form = new sfFormLanguage($this->getUser(), array('languages' => array('en', 'fr')));
    unset($this->form[$this->form->getCSRFFieldName()]);
  }
}
 
// plugins/sfJobeetJob/modules/sfJobeetLanguage/actions/actions.class.php
class sfJobeetLanguageActions extends sfActions
{
  public function executeChangeLanguage(sfWebRequest $request)
  {
    $form = new sfFormLanguage($this->getUser(), array('languages' => array('en', 'fr')));
    unset($form[$form->getCSRFFieldName()]);
 
    // ...
  }
}

getCSRFFieldName() restituisce il nome del campo che contiene il token CSRF. Togliendo questo campo, il widget e il validatore associato vengono rimossi.

Rimuovere la cache

Ogni volta che un utente inserisce e attiva un lavoro, la homepage deve essere aggiornata per elencare il nuovo lavoro.

Siccome non abbiamo bisogno che il lavoro appaia in tempo reale nella homepage, la strategia migliore è quella di abbassare il tempo di scadenza della cache a un valore accettabile:

# plugins/sfJobeetJob/modules/sfJobeetJob/config/cache.yml
index:
  enabled:  true
  lifetime: 600

Al posto della configurazione predefinita di un giorno, la cache per la homepage sarà rimossa automaticamente ogni dieci minuti.

Ma se si vuole aggiornare la homepage non appena un utente attiva un nuovo lavoro, basta modificare il metodo executePublish() del modulo sfJobeetJob aggiungendo una pulizia manuale della cache:

// plugins/sfJobeetJob/modules/sfJobeetJob/actions/actions.class.php
public function executePublish(sfWebRequest $request)
{
  $request->checkCSRFProtection();
 
  $job = $this->getRoute()->getObject();
  $job->publish();
 
  if ($cache = $this->getContext()->getViewCacheManager())
  {
    $cache->remove('sfJobeetJob/index?sf_culture=*');
    $cache->remove('sfJobeetCategory/show?id='.$job->getJobeetCategory()->getId());
  }
 
  $this->getUser()->setFlash('notice', sprintf('Your job is now online for %s days.', sfConfig::get('app_active_days')));
 
  $this->redirect('job_show_user', $job);
}

La cache è gestita dalla classe sfViewCacheManager. Il metodo remove() rimuove la cache associata con un URI interno. Per rimuovere la cache per tutti i possibili parametri di una variabile, usare * come valore. Il valore sf_culture=* che abbiamo usato nel codice qui sopra vuol dire che symfony rimuoverà la cache per le homepage Inglese e Francese.

Siccome il gestore della cache è null quando la cache è disattivata, abbiamo inserito la rimozione nella cache in un blocco if.

Testare la cache

Prima di iniziare, occorre cambiare la configurazione per l'ambiente test, per abilitare la cache:

# apps/frontend/config/settings.yml
test:
  .settings:
    error_reporting: <?php echo ((E_ALL | E_STRICT) ^ E_NOTICE)."\n" ?>
    cache:           true
    web_debug:       false
    etag:            false

Testiamo la pagina di creazione di un lavoro:

// test/functional/frontend/jobActionsTest.php
$browser->
  info('  7 - Job creation page')->
 
  get('/fr/')->
  with('view_cache')->isCached(true, false)->
 
  createJob(array('category_id' => Doctrine::getTable('JobeetCategory')->findOneBySlug('programming')->getId()), true)->
 
  get('/fr/')->
  with('view_cache')->isCached(true, false)->
  with('response')->checkElement('.category_programming .more_jobs', '/29/')
;

Il tester view_cache è usato per testare la cache. Il metodo isCached() accetta due booleani:

  • Se la pagina deve essere in cache o no
  • Se la cache è col layout o no

tip

Anche con tutti gli strumenti forniti dal framwork dei test funzionali, è a volte più facile diagnosticare i problemi col browser. È molto facile farlo. Basta creare un front controller per l'ambiente test. Anche i log memorizzati in log/frontend_test.log possono rivelarsi molto utili.

A domani

Come molte altre caratteristiche di symfony, il sotto-framework della cache è molto flessibile e consente allo sviluppatore di configurare la cache a un livello di granularità molto fine.

Domani parleremo dell'ultimo passo del ciclo di vita di un'applicazione: il rilascio su server di produzione.