Steve Frécinaux

PHP: Cache HTTP et fichiers distants

Une des idées soulevées dans le cadre du projet Standardiste était de récupérer régulièrement les actus sur divers sites, par l’intermédiaires des fils RSS. Pour économiser la bande-passante et les traitements inutiles, on aimerait ne pas retélécharger le fichier si son contenu n’a pas changé…

La solution se trouve dans les options de cache fournies par le protocole HTTP. En effet, si vous utilisez un navigateur basé sur Gecko et l’extension LiveHeader, vous avez dû remarquer certains en-têtes HTTP amusants comme Last- Modified ou If-Modified-Since, de même que le code de réponse 304 Not modified.

Comme nous avons décidé de nous baser sur PEAR pour Standardiste, j’ai basé ce morceau de code sur la classe HTTP::Request. À savoir que l’installation d’une classe isolée, moyennant quelques petites retouche au code source au niveau des inclusions, ne nécessite pas d’installation fonctionnelle de PEAR. Cette classe fournit un bon niveau d’abstraction pour les requêtes HTTP en tant que client.

Les en-têtes qui nous intéressent, en tant que client, sont ceux-ci :

  • Date, qui permet de spécifier au serveur la date actuelle sur le client (en l’occurence sur la machine sur laquelle tourne PHP.
  • If-Modified-Since et If-None-Match, qui vont nous permettre de faire une requête conditionnelle. Si la page n’a pas été modifiée depuis la date spécifiée par le premier en-tête, le serveur renverra le contenu du second dans un en-tête ETag. Sans If-None-Match, If-Modified-Since reste sans effet.
  • Accept et Accept-Charset nous permettent de définir les formats de fichiers préférés et les jeux de caractères gérés.

    $last = […]; // Date de la dernière mise à jour du fichier à vérifier $accept = ‘application/xml, text/xml, text/plain;q=0.3, /;q=0.1’; // Formats que l’on demandera en priorité au serveur

    $r =& new HTTP_Request($url); $r->addHeader(‘Accept-Charset’, ‘iso-8859-1, utf-8;q=0.7’); // Préférences en matière de jeu de caractère $r->addHeader(‘Accept’, $accept); if ($last) { $r->addHeader(‘Date’ , httpDate(time())); $r->addHeader(‘If-Modified-Since’, httpDate($last) ); $r->addHeader(‘If-None-Match’ , ‘”‘.md5(uniqid(‘’).’”’); $r->addHeader(‘Cache-Control’ , ‘max-age=0’); } // Si une erreur se produit if (PEAR::isError($x = $r->sendRequest())) trigger_error(‘Erreur’); if ($r->getResponseCode() != 200) trigger_error(‘Not Modified’); // Sauvegarder date de mise à jour. ($last)

Vous pouvez remarquer, dans le dernier test, que l’on ne traîte ici le résultat que si le code de réponse est le code 200 Ok, qui est le code renvoyé habituellement par une requête réussie. En théorie, il faudrait plutôt vérifier le contenu de l’en-tête de réponse ETag en conjonction avec le code de réponse. En effet, est passé ici sous silence le code 302 Found, par exemple, qui lui aussi peut être renvoyé lors d’une requêtre fructueuse.

Les dates que l’on envoie doivent être telles que définies dans la RFC. Ceci permet de générer une telle date :

function httpDate($time) {
   return gmdate('D, d M Y H:i:s \G\M\T', $time);
}

Et pour terminer, nous récupérons le contenu renvoyé par la requête. Comme nous avons décidé de signaler au serveur que nous acceptons le format UTF-8, il nous faut le décoder, si besoin est, pour ne pas provoquer de bizarrerie avec l’autre format ISO-8859-1. Pour cela, il suffit d’analyser le contenu de l’en-tête Content-Type. Attention, dans le cas de formats de fichiers comme XML ou HTML, la détection du jeu de caractère peut être plus compliquée que ceci (par exemple, vérifier la ligne d’en-tête <?xml ?> dans le premier cas).

$body = $r->getResponseBody();
if (strpos(strtolower($r->getResponseHeader('Content-Type')), 'charset=utf-8') !== false) {
   $body = utf8_decode($body);
}

Je ne suis pas certain que tout ceci soit rigoureusement exact (j’ai par exemple un léger doute sur l’emploi de l’en-tête Date), mais ceci semble bien fonctionner pour ce qui est de mises à jour. Je vous fournis d’ailleurs ici une petite classe censée permettre l’accès à une copie locale d’un fichier distant, en vérifiant l’existance d’une éventuelle mise à jour de celle-ci.

Notez aussi que ceci ne fonctionne que dans le cas où le serveur gère le cache HTTP, ce qui est rarement le cas des fichiers générés par PHP, par exemple… Si vous voulez que votre page dynamique gère le cache, je vous renverrai au billet Cachez-moi ça ! sur Dreams4Net