In questo articolo ci occuperemo di introdurre i lettori un argomento molto interessante: il caching delle pagine web. Quando un browser richiede ad un webserver una pagina contenente del codice lato server (PHP nel nostro caso), il webserver si deve occupare di eseguirla e restituire il risultato dell'operazione al client. Durante il processo di esecuzione solitamente avvengono diversi accessi a disco o a database, vengono eseguiti più o meno complessi algoritmi. Il tempo richiesto per l'esecuzione della pagina viene sommato a quello richiesto per il download ed aumenta l'attesa dell'utente. Oltretutto il processo di esecuzione di codice lato server comporta un "notevole" dispendio di risorse.
Tramite le operazioni di caching, che si occupano di salvare il risultato del processo di esecuzione sul server, possiamo minimizzare i tempi di attesa e diminuire notevolmente il carico di lavoro della nostra macchina remota. Esistono svariate soluzioni per eseguire il caching di una pagina, da soluzioni molto semplici ad altre molto complesse. In questo articolo il mio unico intento è quello di introdurre l'argomento proponendovi del codice semplice da comprendere e riprodurre. Starà a voi adattare quello che avete imparato a casi reali.
Il caching in pratica
Come accennato precedentemente le operazioni di caching possono essere implementate in diversi modi. Qualunque sia la nostra implementazione dobbiamo tenere conto di alcune informazioni che ci saranno molto utili per sviluppare il sistema di caching:
- Conviene eseguire il caching di pagine che cambiano il loro contenuto sporadicamente o con bassa frequenza.
- Le pagine salvate dovranno avere un "periodo di vita", dopo il quale dovranno essere sostituite con una versione più recente della stessa. Questo per evitare il mancato aggiornamento di alcune pagine. Nessuno vieta comunque di utilizzare periodi di scadenza molto lunghi in casi particolari.
- Il contenuto delle pagine varia spesso in base ad alcune condizione dovute a dati ricevuti in input dalla pagina. Questi dati possono provenire da variabili superglobali, file o molto altro. Per questo motivo è necessario fornire un sistema che permetta di eseguire il caching di pagina più di una volta, associando ad ogni salvataggio le informazioni relative ai dati provenienti dall'esterno.
- Spesso non è necessario o conveniente eseguire il caching completo della nostra pagina. Ci possono essere situazioni in cui dei blocchi di html rimangono invariati per molto tempo, mentre altri vengono aggiornati continuamente. Per questo motivo sarebbe opportuno che il nostro sistema permettesse il caching di porzioni di pagina;
- Il caching non è limitato solamente a codice HTML: è possibile (e molto conveniente) eseguire il caching di immagini generate con librerie GD, di file PDF e molto altro ancora.
Tenendo presente queste considerazioni iniziamo con una semplicissima implementazioni di caching che chiarisca a tutti le idee; in questa implementazione useremo le funzioni di output buffering di PHP per salvare l'output generato dal nostro codice e le funzioni di I/O per salvare e leggere i dati da disco.
<?php
$page_id = "miapagina.php";
$timeout = 60;
$path = "./chached/".$page_id;
if(!file_exists("./chached /"))
mkdir("./chached /");
if(file_exists($path) and filemtime($path) + $timeout > time()) {
$result = readfile($path);
if($result)
exit();
}
set_time_limit(0);
ob_start();
for($i = 0; $i < 50; ++$i){
sleep(1);
echo date("h : i : s")."
";
}
$output = ob_get_flush();
$fp = fopen($path, "w");
fwrite($fp, $output, strlen($output));
fclose($fp);
?>
Il codice è molto semplice: le prime tre righe definiscono tre variabili contenenti rispettivamente un ID univoco per la pagina, i millisecondi di vita della pagina ed il path in cui sarà salvato l'output. Dalla riga 8 alla riga 11 ci occupiamo di controllare che esista la cache e che questa non sia scaduta. In caso affermativo visualizziamo il contenuto della cache ed in caso di visualizzazione avvenuta terminiamo lo script. Nel caso in cui la cache sia scaduta oppure nel caso in cui la lettura della cache non abbia restituito esito positivo, inizializziamo l'output buffering di PHP ed eseguiamo del codice volutamente lento (la prima esecuzione durerà circa 55 secondi). Terminato il blocco di codice centrale, recuperiamo l'output e lo salviamo all'interno della cache.
Come è possibile notare, il caching non è un concetto molto complesso da implementare, e richiede poco lavoro per poter essere reso operativo e funzionante.
Un'implementazione alternativa
Dopo aver presentato un'implementazione introduttiva del caching di una pagina, vi mostrerò come è possibile, con poco e semplice codice, implementare un sistema leggermente più avanzato, che permetta di rispondere a tutti i punti elencati nel paragrafo precedente.
Lo script seguente fornirà una semplice libreria composta da due classi che si occuperanno di gestire il caching delle vostre pagine ed alcune funzioni di utilità. La prima classe, CacheContent, servirà per la gestione dell'output di una sezione di codice PHP: si occuperà di creare la cache, visualizzarla, settare eventuali modificatori che tengano traccia dell'input ricevuto dalla pagina e settare il timeout della cache. La seconda classe, HtmlCache, si occuperà di gestire i dati salvati in cache, generando istanze di CacheContent quando richiesto.
<?php
define("INVALID_DIR", 501);
define("FLAGS_NONE", 0x0);
define("USE_GET", 2 << 0);
define("USE_POST", 2 << 1);
define("USE_CUSTOM", 2 << 2);
define("CACHE_CREATED", 2);
define("CACHE_READ", 1);
function CacheError($code){
switch($code){
case INVALID_DIR:
$error = "Ivalid cache directory specified.";
break;
default:
$error = "Unknown Error";
break;
}
die("Cache error: ".$error);
exit();
}
class CacheContent {
var $working_directory = "";
var $name_modifiers = array();
var $timeout = 0;
var $file_extension = "";
var $fp = null;
var $type = -1;
function CacheContent($dir, $ext, $n){
$this->working_directory = $dir."/".basename($_SERVER['PHP_SELF'])."/";
if(!file_exists($this->working_directory))
mkdir($this->working_directory);
array_push($this->name_modifiers, $n);
$this->file_extension = $ext;
}
function SetNameModifier($mod){
$new = array();
foreach($mod as $key=>$val){
if(is_array($val)){
$this->SetNameModifier(array_merge(array($key), $val));
}else array_push($new, $key.$val);
}
array_merge($this->name_modifiers, $new);
}
function SetTimeout($timeout){
$this->timeout = $timeout;
}
function Create(){
$expired = false;
$file_id = md5(implode("", $this->name_modifiers));
$file_name = $file_id.".".$this->timeout.$this->file_extension;
if(file_exists($this->working_directory.$file_name)){
if(time() > filemtime($this->working_directory.$file_name)+$this->timeout){
$expired = true;
}else echo file_get_contents($this->working_directory.$file_name);
}else $expired = true;
if($expired){
$this->fp = fopen($this->working_directory.$file_name, "w");
ob_start();
$this->type = CACHE_CREATED;
return;
}
$this->type = CACHE_READ;
}
function GetType(){
return $this->type;
}
function Stop(){
if(!is_null($this->fp)){
fwrite($this->fp, ob_get_contents());
fclose($this->fp);
ob_end_flush();
}
}
}
class HtmlCache {
var $cached_dir = "";
var $cached_files_ext = "";
var $cached_global_timeout = "";
var $cached_counter = 0;
function HtmlCache($dir, $ext, $timeout){
$dir = $_SERVER['DOCUMENT_ROOT'].dirname($_SERVER['PHP_SELF'])."/".$dir;
if(!file_exists($dir)){
mkdir($dir."/".basename($_SERVER['PHP_SELF'])."/", 0700);
}
$this->cached_dir = $dir;
$this->cached_files_ext = $ext;
$this->cached_global_timeout = intval($timeout, 10);
}
function Create($flags = 0x0, $timeout = null, $custom_data = null){
$cache = new CacheContent($this->cached_dir, $this->cached_files_ext, $this->cached_counter);
$this->cached_counter++;
if($flags & USE_GET){
global $_GET;
$cache->SetNameModifier($_GET);
}
if($flags & USE_POST){
global $_POST;
$cache->SetNameModifier($_POST);
}
if($flags & USE_CUSTOM){
$custom_data = (is_array($custom_data) && $custom_data != null) ? $custom_data : array($custom_data);
$cache->SetNameModifier($custom_data);
}
$timeout = is_numeric($timeout) ? $timeout : $this->cached_global_timeout;
$cache->SetTimeout($timeout);
$cache->Create();
return $cache;
}
}
?>
Descriviamo brevemente il codice presentato qui sopra:
- I primi define si occupano di definire delle costanti che verranno utilizzate dalla libreria; abbiamo definiti delle tipologie di errore, dei flag utilizzati per comunicare alla libreria che dati utilizzare per la generazioni di ID univoci in base ad input esterno, e due stati utilizzati per comunicare allo script se la cache è stata creata oppure letta.
- La funzione CacheError si occupa di controllare che errore è stato generato e di eseguire un report differente in base all'errore; per ora gestisce solamente il caso in cui sia stata specificata una directory errata per contenere le cache.
- Il costruttore della classe CacheContent accetta tre parametri rappresentati rispettivamente la directory in cui contenere la cache, l'estensione dei file salvati in cache, ed un codice utilizzato per indicare univocamente la cache. Questi dati vengono salvati e viene creata la directory di lavoro (in cui saranno salvate le cache della pagina corrente).
- Il metodo SetNameModifier accetta un unico parametro rappresentato da un array contenete i dati utilizzati per indicare univocamente una porzione di cache. A questo metodo, per esempio, potrà essere passato l'array $_POST per salvare una cache differente in base ai dati inviati tramite form.
- Il metodo SetTimeout setta il timeout della cache.
- Il metodo Create si occupa di generare il nome del file utilizzato per la cache basandosi sui modificatori eventualmente assegnati, controllare che la cache non sia scaduta ed in caso affermativo visualizzarla. In caso negativo svuota la vecchia cache ed inizializza l'output buffering di PHP al fine di poter salvare l'ouput generato successivamente. Il metodo si occupa anche di settare il tipo di CacheContent.
- Il metodo Stop, infine, si occupa di salvare l'output generato da PHP solamente nel caso in cui la cache debba essere aggiornata.
- Il costruttore della classe HtmlCache accetta tre parametri: i primi due rappresentano la directory in cui salvare la cache e l'estensione dei file di cache, mentre il terzo rappresenta il secondi di vita della cache utilizzati nel caso in cui il timeout non sia specificato nel metodo create.
- Il metodo Create della classe HtmlCache accetta tre parametri: il primo rappresenta una serie di flag utilizzati per informare la libreria di quali dati utilizzare per la generazione del modificatore, il secondo rappresenta il timeout della singola sezione di cache e l'ultimo rappresenta un array di dati custom utilizzati per la generazione del modificatore nel caso in cui il primo parametro contenga il flag USE_CUSTOM. Il metodo crea un CacheContent e lo restituisce al chiamante.
Per testare la libreria utilizziamo questo semplice script:
<?php
function getmicrotime(){
list($usec, $sec) = explode(" ",microtime()) ;
return ((float)$usec + (float)$sec) ;
}
include_once("HtmlCache.php");
$time_start = getmicrotime();
$c = new HtmlCache("cached",".html", 60);
$section = $c->Create();
if($section->GetType() == CACHE_CREATED){
for($i =0 ; $i < 10000; $i++){
echo "ciao<br>";
}
}
$section->Stop();
echo $section->GetType()."<br>";
$time_end = getmicrotime();
$time = $time_end - $time_start;
echo ("
Eseguito in $time secondi
");
?>
Come potete notare viene eseguito il caching solamente del codice restituito dal ciclo for; per eseguire il caching di altre porzioni di codice, basta generare una nuova sezione utilizzando $c->Create() e gestirla come nel seguente esempio, in cui vengono generate due porzioni di cache di durata 60 e 30 secondi:
<?php
include_once("HtmlCache.php");
set_time_limit(0);
$c = new HtmlCache("cached",".html", 60);
$section = $c->Create(FLAGS_NONE, 60);
if($section->GetType() == CACHE_CREATED){
echo date("h:i:s")."<br>";
for($i =0 ; $i < 10; $i++){
sleep(1);
echo "ciao<br>";
}
}
$section->Stop();
$section2 = $c->Create(FLAGS_NONE, 30);
if($section2->GetType() == CACHE_CREATED){
echo date("h:i:s")."<br>";
for($i =0 ; $i < 10; $i++){
sleep(1);
echo "ciao 2<br>";
}
}
$section2->Stop();
?>
Conclusioni
Concludo qui la mia introduzione al caching. Dopo la lettura di questo articolo dovreste aver compreso come sviluppare ed utilizzare un semplice sistema di caching da affiancare al vostro sito web o alla vostra applicazione. Ovviamente l'argomento non si ferma qui, e starà a voi implementare nuove soluzioni per gestire i problemi che via via si poranno sulla vostra strada.