För ett nuvarande projekt hade jag behov av att cacha data, men vi använder oss av ett specialskrivet framework och ville inte implementera caching på hela webbplatsen. Istället ville jag ha möjlighet att cacha delar av resultatet för att snabba upp sidvisningen. Anledningen är att jag behöver vissa mycket statistik på startsidan, denna statistik uppdateras inte särskilt, och i det här fallet är det inte så viktigt att datan är helt aktuell.
De krav jag hade när jag började kolla in i problemet är
- Skall fungera på delar av resultatet (inte spara hela webbplatsen)
- Resultatet skall sparas till statiska html-filer
- Resultatet skall skapas när det efterfrågas unikt för varje besökare
- Så liten påverkan på övrig kod som möjligt
Jag började så klart med att söka runt efter färdiga lösningar och hittade då en tråd på stackoverflow (How do I implement a HTML cache for a PHP site?), de flesta svaren är helt färdiga template system med inbyggd caching men eftersom vi använder ett eget framework och i dagsläget inte har tid att införa detta var det inte aktuellt för mig. Andra möjligheter var att använda proxy cache eller lösningar som Memcache eller APC men webbplatsen skall installeras på ett vanligt webbhotell och jag letade egentligen efter en enklare lösning än dessa.
Istället fastnade jag för PHPs inbyggda funktion för output-buffering vilket gör det möjligt att spara outputdatan i minnet och skriva den till fil för att vid nästa sidvisning läsa den från filen istället.
ob_start();
echo 1+2;
$cache = ob_get_contents();
ob_end_clean();
echo $cache;
Så ser det ut i sin simplaste form, där $cache
nu håller datan, i det här fallet "3"
, som skall sparas. Tjusningen är att den även sparar all output, html kod och allt annat som finns mellan ob_start() och ob_get_content(). För att göra detta så smidigt som möjligt stoppade jag in det i en egen class PartialCache, jag har laddat upp classen i sin helhet på github för den som är intresserad. Här kommer också en kort genomgång och huvudpunkterna. Koden tillhandahålls open source och ”as is”, den behöver säkerligen modifieras lite innan den fungerar för dig.
public function __construct($site, $key, $disable=false)
{
if (is_array($key)) {
$key = implode($key,"_");
}
if ($disabled)
{
$this->filename = dirname(__FILE__) ."/../cache/". $site ."/". $key .".html";
$this->site = $site;
$this->key = $key;
}
}
Jag använder mig av mappstrukturen samt filnamnen för att spara cachad data till disk, $site blir mappen och $key filnamnet, har man flera värden man vill skicka med läggs dessa ihop med en linje.
public function is_cached()
{
$limit = 60*60*24;
return file_exists($this->filename) && (time() - $limit) > filemtime($this->filename);
}
För att kolla ifall datan finns i cachen kontrollerar jag enbart ifall filen finns på disk och i mitt fall att filen inte är mer än 1 dag gammal.
public function start()
{
if (!$this->is_cached()) {
ob_start();
return true;
} else {
return false;
}
}
public function render()
{
if ($this->is_cached()) {
$cache = file_get_contents($this->filename);
echo $cache;
echo '<div class="info-box">'
. sprintf($this->text,
strftime("%Aen den %e %B kl %H:%M", filemtime($this->filename)))
. sprintf($this->link, $this->site, $this->key)
. '</div>';
} else {
$cache = ob_get_contents();
ob_end_clean();
echo $cache;
$file = fopen($this->filename, 'w');
if (is_writable($this->filename)) {
fwrite($file, $cache);
fclose($file);
} else {
echo '<div class="info-box">Kunde <strong>inte skapa cache</strong>, troligen felaktiga rättigheter på servern. Vänligen kontakta systemadministratören.</div>';
}
}
}
Jag har sedan de två funktionerna start och render, som ifall det inte finns någon cache startar output-buffern och i render sparar denna till fil. Finns den redan i cache retunerar funktionen false vilket indikerar att koden inte behövs köras igen, istället läser render från fil och skriver ut denna. Exempel på hur koden kan användas
<?php
$cache = new PartialCache("statistics", $userid);
if ($cache->start()):
?>
<h1>Resultatet</h1>
<p><?php echo $result;<?></p>
<?php
endif;
$cache->render();
?>
Så gör man för att enkelt cacha delar av sitt resultat unikt för varje besökare.