Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Il Framework MVC Taste: il caching delle informazioni

Costruiamo il nostro framework MVC: integriamo nella nostra applicazione un modulo che consente di mantenere in cache i risultati di diverse operazioni
Costruiamo il nostro framework MVC: integriamo nella nostra applicazione un modulo che consente di mantenere in cache i risultati di diverse operazioni
Link copiato negli appunti

Questo articolo fa parte di una serie dedicata alla programmazione di un framework MVC personalizzato in PHP. L'autore ha chiamato questo framework "Taste". Gli altri articoli della serie sono disponibili nella categoria Taste framework di php.html.it.

Con l'articolo di oggi ci prendiamo una piccola pausa per quanto riguarda
l'avanzamento dei lavori e ci soffermiamo su un'operazione di supporto che è
ormai fondamentale per tutte le applicazioni ed i siti web con alto traffico o
un basso numero di risorse: il caching.

Con caching si intendono fondamentalmente una serie di
operazioni che servono a salvare i risultati intermedi di un'elaborazione e
restituirli nel caso in cui venga richiamata la stessa operazione senza
effettuare nuovamente i calcoli. I browser internet per esempio immagazzinano su
disco, per un periodo di tempo controllato dagli header http e dalle
impostazioni di configurazione, i file recuperati dalla rete in modo da non
dover scaricare ogni volta lo stesso file nel caso di richieste successive.

Aggiungeremo quindi il supporto al caching sui due fronti che sono
solitamente quelli più sottoposti a stress: il routing e l'output dei
controller, implementando le classi necessarie e modificando leggermente il file
di bootstrap per riflettere i cambiamenti.

Come sempre potete trovare i sorgenti allegati a questo articolo, nella
sezione download.

Caching delle operazioni di routing

Come prima cosa iniziamo implementando il caching sulle operazioni di
routing
. Ogni qualvolta viene richiesta una pagina il router
implementato deve effettuare un controllo su tutte le regole di routing
specificate, eseguendo delle espressioni regolari per verificare quale regola
risponde all'URL richiesto e recuperare il controller desiderato. Nel caso di un
grosso numero di pagine ed un alto numero di richieste le operazioni possono
essere molto gravose.

Per evitare questo implementeremo un router che salva i risultati delle
singole operazioni di routing all'interno di una cache su disco (che sarà
rappresentata da un array associativo) che verrà interrogata prima di
confrontare l'url recuperato con le regole specificate nel file di bootstrap. Le
chiavi di ogni record della cache saranno gli URL che provengono dalla
richiesta, comprensivi di eventuali parametri.

L'implementazione di questo router (che potete trovare all'interno del file
taste/mvc/routing/CachedRouter.php
) estende la classe astratta
Router
ma utilizza un Router come base, che verrà richiamato nel caso in
cui non venga trovato nella cache un record corrispondente alla richiesta.

class CachedRouter extends Router
{
    private $cache_file;
    private $cache;
    private $base_router;
    
    public function __construct(Router $base_router)
    {
        parent::__construct();
        
        $this->base_router = $base_router;
        $this-->cache = array();
        $cache_dir = $this-->config-->get('cacheDir', 'static/cache/');
        if($cache_dir{0} != '/')
            $cache_dir = realpath(TASTE_DIR."/../".$cache_dir);
        
        $this-->cache_file = $cache_file = $cache_dir."/cached_router_cache.php";
        if(!file_exists($cache_file))
        {
            $fp = fopen($cache_file, "w+");
            flock($fp, LOCK_EX);
            fwrite($fp, "");
            flock($fp, LOCK_UN);
            fclose($fp);
        }else
            $this-->cache = require_once($cache_file);
    }
    
    public function route($url)
    {
        if(!array_key_exists($url, $this-->cache))
        {
            $cpath = $this-->base_router-->route($url);
            if(is_null($cpath))
                return null;
            
            $this-->cache[$url] = $cpath;
            
            $fp = fopen($this-->cache_file, "w+");
            flock($fp, LOCK_EX);
            fwrite($fp, "");
            flock($fp, LOCK_UN);
            fclose($fp);
        }
        
        return $this-->cache[$url];
    }
}

La cache è salvata all'interno di un file PHP, e viene importata in fase di
inizializzazione ed aggiornata con il risultato del processo di routing nel caso
in cui non vi sia ancora alcun record corrispondente alla richiesta effettuata.

Effettuare il caching in questo modo per questo genere di operazione non è
forse la soluzioni migliore; sarebbe preferibile utilizzare qualche sistema che
mantenga l'array in memoria condivisa tra le varie richieste (come ad esempio
memcached).

La cache su disco

Prima di descrivere come implementare il caching delle risposte generate dai
controller ci occupiamo di vedere come si è deciso di rappresentare la cache su
disco. In questo caso ogni risposta verrà salvata all'interno di un file
differente, posizionato nella cartella di caching specificata nel file di
configurazione. La cache nel suo insieme è rappresentata da un'istanza della
classe Cache, mentre ogni singolo elemento salvato da un'istanza
della classe Slot. Entrambi queste classi sono definite all'interno
della cartella taste/cache.

La classe Cache espone dei metodi per recuperare, eliminare e
salvare degli Slot. Automaticamente si occupa di controllarne la
validità ed eventualmente di rimuovere da disco i file corrispondenti nel caso
in cui risultino scaduti. Il periodo di vita di uno slot è definito in modo
univoco in fase di costruzione della classe Cache, e può
eventualmente essere variato.

La classe Slot invece contiene i dati relativi ad un determinato
record della cache, che può essere salvato su disco dalla classe Cache
e recuperato quando richiesto.

$key = 'una-chiave-possibilmente-univoca';
$timeout = 10;

$cache = new Cache('/tmp/cache', $timeout);
$slot = $cache-->find($key);

if(is_null($slot))
{
        $data = COMPUTE();
    
    $slot = new Slot($key, $timeout);
    $slot-->setData($data);
    
    $cache-->store($slot);
}

echo $slot-->getData();

Quando vogliamo utilizzare la cache, le operazioni da eseguire sono molto
semplici:

  • Inizializziamo la cache con la directory in cui salvare gli slot ed il tempo
    di vita di ogni singolo slot;
  • richiedere alla cache lo slot necessario;
  • se lo slot esiste utilizzare il metodo getData per recuperarne
    il valore e gestirlo come necessario;
  • se lo slot non esiste, computarne il valore, crearne uno nuovo e salvarlo
    all'interno della cache.

L'applicazione CacheApplication

Utilizzando la classe Cache e la classe Slot
precedentemente implementate, possiamo sviluppare l'applicazione che si occuperà
di effettuare in automatico il caching delle risposte generate in base alle
richieste. La classe in questione (che potete trovare in
taste/applications/CacheApplication.php
) deriva da Application
e viene utilizzata come middleware che opera in modo trasparente su un'altra
applicazione, che viene interrogata per recuperare i dati nel caso in cui la
cache per una determinata richiesta non esista oppure sia scaduta.

class CacheApplication extends Application
{
    public function run(Request $request)
    {
        $key = md5($request-->getPathInfo());
        $timeout = intval($this-->config-->get('timeout', 5));
        $cache_dir = $this-->config-->get('cacheDir', 'static/cache');
        if($cache_dir{0} != '/')
            $cache_dir = realpath(TASTE_DIR."/../".$cache_dir);
        
        $cache = new Cache($cache_dir, $timeout);
        $slot = $cache-->find($key);
        
        if(is_null($slot))
        {
            $response = $this-->runParent($request);
            
            $slot = new Slot($key, $timeout);
            $slot-->setData($response-->getContent());
            
            $cache-->store($slot);
        }else
            $response = new Response($slot-->getData());
        
        $response-->setHeaderIfUnset('ETag', $slot-->getId());
        $response-->setHeaderIfUnset('Last-Modified', date("r"));
        $response-->setHeaderIfUnset('Expires', date("r", $slot-->expireTime()));
        $response-->setHeaderIfUnset('Cache-Control', "max_age=".$timeout);
        
        return $response;
    }
}

Sfruttando il sistema di middleware implementato quando abbiamo discusso
della classe Application, possiamo incapsulare un'applicazione
qualsiasi con una CacheApplication che salterà automaticamente il
processo di elaborazione della risposta nel caso in cui sia presente uno slot su
disco (non scaduto) corrispondente all'URL richiesto.

In aggiunta alla generazione automatica del response, ho inserito degli
header HTTP che informano il browser del fatto che la risposta che riceverà
potrà essere salvata in locale per un periodo di tempo pari al tempo di vita
dello slot recuperato. Gli header HTTP ed il processo di caching possono
sicuramente essere ottimizzati e migliorati in modo da comportarsi
differentemente in base alla tipologia di richiesta effettuata o al controller
che si prende in esame; per esempio potrebbe essere interessante sviluppare una
tipologia particolare di controller i cui risultati delle azioni richiamate non
verranno salvati in cache. Oppure si potrebbe utilizzare la query string come
discriminante per decidere se utilizzare o meno la cache.

Come ultima operazione per controllare definitivamente che i processi di
caching funzionino correttamente, dobbiamo modificare leggermente il file di
bootstrap in modo da aggiungere le chiamate alle classi CachedRouter
e CacheApplication.

require_once 'taste.php';
require_once 'taste/Server.php';
require_once 'taste/mvc/routing/ExplicitRouter.php';
require_once 'taste/mvc/routing/CachedRouter.php';
require_once 'taste/applications/MVCApplication.php';
require_once 'taste/applications/CacheApplication.php';

$router = new ExplicitRouter(array(
    "^list/?$"  =>  "test.TestController.show",
    "^list/(d{4})/?$"  =>  "test.TestController.show",
    "^list/(d{4})/(d{2})/?$"  =>  "test.TestController.show",
    "^list/(d{4})/(d{2})/(d{2})/?$"  =>  "test.TestController.show",
));

$router = new CachedRouter($router);

$server = new Server(
            new CacheApplication(
                new MVCApplication($router)));

$server-->run();

Conclusione

Abbiamo terminato questa piccola ma importate parentesi che ha anche
dimostrato come sia semplice estendere le applicazioni in modo da farle aderire
alle nostre esigenze. Nel prossimo articolo inizieremo a discutere del sistema
di template e di come strutturare un template engine basato su XML.

Ti consigliamo anche