Gijs van Tulder

Cache-vriendelijke pagina’s in PHP

In de meeste gevallen zul je niet willen dat de browser de uitvoer van je PHP-scripts in de cache opslaat. Het moet gewoon iedere keer weer worden opgevraagd. In sommige gevallen is het echter beter om de browser wel te laten cachen. Bijvoorbeeld als je PHP een plaatje laat verkleinen. In dat geval zul je de browser een beetje moeten helpen, zodat het plaatje wel wordt opgeslagen in de cache.

Doel

We zijn op zoek naar een manier om het voor de browser mogelijk te maken de uitvoer van PHP-scripts in de cache op te slaan. Daardoor hoeft de bezoeker niet steeds te wachten tot het bestand opnieuw is binnengehaald en heeft je server het minder druk.

Er zit ook een nadeel aan het gebruik van de cache. Omdat bestanden door de browser worden bewaard, zal die browser niet iedere keer op de server kijken of er iets veranderd is. Als je pagina net veranderd is, zullen de bezoekers nog steeds de oude versie zien. (Als ze eerder zijn geweest, dat wel.) Voor een forum is het dus niet handig om de cache in te schakelen, voor dingen die zelden veranderen wel. Je zou daarbij kunnen denken aan stylesheets en plaatjes die je met behulp van PHP een klein beetje aanpast.

Middel 1: de serverheaders

Iedere pagina die je browser van de server ontvangt, wordt voorafgegaan door een aantal headers. Deze headers geven meer informatie over de opgevraagde pagina. De headers van een html-pagina op mijn server zagen er vandaag bijvoorbeeld zo uit:

HTTP/1.1 200 OK
Date: Mon, 17 Mar 2003 13:09:54 GMT
Server: Apache/1.3.27 (Unix) mod_ssl/2.8.11 OpenSSL/0.9.6g PHP/4.3.1
Last-Modified: Mon, 24 Feb 2003 13:54:55 GMT
Content-Type: text/html

Afgezien van de eerste header, dat is een speciale, kun je zien dat deze headers op dezelfde manier zijn opgebouwd: de naam van de header, een dubbele punt en dan de informatie. Van één van deze headers, Last-Modified:, zullen we in dit artikel gebruik gaan maken.

N.B.: De headers worden niet weergegeven door je browser. Als je toch de headers van je scripts wilt bekijken, dan kun je in dit artikel lezen hoe je dat kunt doen.

Last-Modified:

De eerste header die we nodig hebben is de Last-Modified:-header. Deze header heeft als inhoud een datum en tijd. Deze datum en tijd geven aan wanneer het bestand in kwestie voor het laatst gewijzigd is. Omdat de browser deze informatie gebruik bij het bijhouden van de cache, moeten we hiervoor straks iets zinvols terugsturen.

Cache-Control:

Met de Cache-Control:-header kunnen we de browser opdracht geven de pagina wel, of juist niet te cachen. Ook kunnen we aangeven hoe lang het gecachede bestand dan gebruikt mag worden. Wij zullen in dit artikel twee van die opdrachten gebruiken:

HTTP/1.1

De laatste header die we zullen gebruiken is de header die we zojuist als 'speciaal' terzijde hebben geschoven. HTTP/1.1 geeft aan welke versie van het HTTP-protocol er gebruikt wordt, in dit voorbeeld versie 1.1. Achter deze header komt een driecijferige code en een verklaring van die code. Dit is de statuscode. Bij code 200 OK geeft de server aan dat de pagina goed wordt verstuurd. Bij code 404 Not Found, vast wel bekend, zegt de server dat de gevraagde pagina niet is gevonden.

In ons geval zijn we vooral benieuwd naar code 302 Not Modified. Hiermee vertellen we de browser dat de pagina niet is veranderd. In dat geval wordt dan ook geen pagina meegestuurd, want die heeft de browser immers al. Dit is precies waar we naar op zoek zijn.

Middel 2: De browserheaders

Met het bestuderen van de headers die de server kan versturen, zijn we er nog niet. De browser stuurt namelijk ook headers. Deze headers zien er hetzelfde uit als de serverheaders, alleen hebben ze andere namen. In dit artikel hebben we voornamelijk belangstelling voor n header.

If-Modified-Since:

Deze header wordt gevolgd door een datum. Met de If-Modified- Since:-header stelt de browser de server een vraag: geef deze pagina alleen, als hij sinds de opgegeven datum is veranderd. Is dat niet het geval, stuur mij dan alleen de statuscode 302 Not Modified (zie boven). Dat scheelt namelijk tijd: het hele bestand hoeft niet meer verzonden te worden.

Deze header zullen we straks ook tegenkomen. Is de cache van de browser, volgens de max-age, verouderd, dan zal de browser ons deze header toesturen. Als ons script daar dan goed op antwoordt, dus eventueel code 302 terugstuurt, dan is ons cache-systeem klaar.

Uitvoering

Nu we weten hoe de verschillende headers precies werken, kunnen we die kennis omzetten in een PHP-script. De regels die volgen kun je gewoon verwerken in ieder PHP-script dat je wilt laten cachen. Let er daarbij wel op dat PHP alleen headers kan versturen zolang er nog geen gewone tekst verstuurd is, dus je moet deze opdrachten boven de eerste keer echo plaatsen.

De tijd

Je zult eerst moeten zorgen dat je de datum achterhaalt waarop de pagina voor het laatst gewijzigd is. Bij bestanden kun je dat opvragen met filemtime(). In dit voorbeeld slaan we die tijd in ieder geval op in de variabele $tijd. Die tijd moeten we vervolgens omzetten in het formaat dat volgens het HTTP-protocol gewenst is. Die omgezette tijd slaan we op in $last_modified, en zullen we straks gebruiken bij het samenstellen van de headers.

// tijd omzetten naar: Mon, 17 Mar 2003 13:12:25 GMT
$last_modified = gmdate('D, d M Y H:i:s',$tijd).' GMT';

If-Modified-Since?

Nu we de tijd bepaald hebben, kunnen we kijken of de browser een If-Modified-Since:-header heeft meegestuurd. Is dat het geval, en komt die datum overeen met onze $last_modified , dan hoeven we de pagina niet opnieuw te sturen. De regel HTTP/1.1 304 Not Modified volstaat.

De headers die de browser ons stuurde, kunnen we terugvingen in de array $_SERVER. De header die wij willen opvragen heet dus $_SERVER['IF_MODIFIED_SINCE'].

if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
    if ($_SERVER['HTTP_IF_MODIFIED_SINCE'] == $last_modified) {
        // de cache is nog steeds goed genoeg
        header("HTTP/1.1 304 Not Modified");
        header("Cache-Control: max-age=86400, must-revalidate");
        exit;
    }
}

Na de HTTP/1.1-header sturen we nog een header: Cache-Control. Dat is precies dezelfde header als de header die we zo zullen versturen. We zullen er dan wat langer naar kijken.

Wel versturen

Als het script nu nog steeds wordt uitgevoerd, betekent dat dat we de browser wel de hele pagina moeten sturen. Ofwel omdat de cache verouderd is, ofwel omdat de browser de pagina nog niet gecached heeft.

In dit geval zouden we eerst een HTTP/1.1 200 OK- header moeten versturen, we sturen immers de hele pagina. Gelukkig doet PHP dat al voor ons, dus hoeven we ons daar niet mee te bemoeien.

Interessanter is het om te kijken naar de twee headers die we wel versturen: Cache-Control: en Last-Modified: . Cache-Control: wordt, zie boven, gevolgd door de opdracht max-age en een aantal secondes. Dit is de tijd dat een browser zonder zich met de server te bemoeien gewoon de gecachede versie van de pagina mag laten zien. In mijn voorbeeld gebruik ik 86400, dat is 60 x 60 x 24 = 24 uur. Je kunt er natuurlijk zelf iets anders invullen. Na max-age volgt zoals gezegd nog de opdracht must-revalidate.

Last-Modified: wordt gevolgd door de datum waarop de pagina voor het laatst is veranderd. Die datum hebben we net al vastgesteld, en is te vinden als $last_modified.

header('Cache-Control: max-age=86400, must-revalidate');
header('Last-Modified: '.$last_modified);

De informatie

Nadat de headers verstuurd zijn, kun je nu de informatie zelf versturen. Wat die informatie is, mag je zelf bepalen.

Tot slot

Je kunt nu zorgen dat je pagina's en bestanden gewoon gecached worden. Zeker als je je plaatjes door PHP laat verzorgen is dat voor je bezoekers erg fijn.

De cache en sessies

Als je de session_*-functies van PHP gebruikt, zul je waarschijnlijk zien dat het script niet goed werkt. Je pagina's willen maar niet in de cache blijven hangen. Dit wordt veroorzaakt door het gebruik van sessies. PHP verstuurt in dat geval juist headers die het cachen tegengaan, en tot dusver lijkt het erop dat het niet-cachen het daarbij wint van het wel-cachen. Wil je laten cachen, dan zul je op die pagina's geen sessions moeten gebruiken.