Le Hollandais Volant

Ma gestion améliorée de cache statique PHP/Apache

Programmation.
Ceci est une implémentation de l’idée que j’avais postée ici et qui concernait une gestion de cache basée sur le .htaccess et les erreur 404.

Le modèle habituel d’un cache statique PHP

Habituellement, pour gérer les caches de fichiers statique, on envoie une requête sur un fichier PHP qui va s’occuper de récupérer une ressource statique et l’envoyer vers le navigateur :

cache.php

<?php
    readfile($fichier_statique);
?>

Le $fichier_statique contient des données HTML déjà prêtes. Normalement, une fois que la page HTML est envoyée au visiteur, elle est oubliée de la mémoire de l’ordi. Si un autre visiteur fait la même requête, le serveur doit tout refaire.

L’idée d’un cache est de stocker le code HTML que l’on a envoyé au visiteur. Comme ça, si un second visiteur fait la même requête, on lui envoie ce qui se trouve en cache, sans avoir à tout recalculer : c’est beaucoup plus rapide.

Dans mon cas, je le fais avec des images d’avatar de chez Gravatar et avec des favicon que je met en face des sites dans mon lecteur RSS. Je ne fais pas une requête vers ces sites, mais vers mon lecteur RSS. C’est lui qui va faire une requête externe, sauver l’avatar ou la favicon localement, puis l’envoyer au client. La fois d’après, il envoie directement l’icône locale au client, sans faire de requête réseau. On gagne beaucoup en performances globales.

Le problème que je vois ici, c’est que le fichier cache.php est appelé pour chaque requête sur un fichier. C’est plus rapide que recalculer une miniature, mais une instance PHP est tout de même créée, ne serait-ce que pour lancer le readfile(), et éventuellement après quelques calculs rapides pour déterminer quel fichier cache on envoie au navigateur.

On peut s’affranchir de cette requête et de PHP.

Utiiliser .htaccess

On va prendre l’exemple du cache pour les images favicon et gravatar, car c’est ce que j’utilise et c’est ce pourquoi j’ai fait cette méthode.

Avant je faisais une requête vers cache.php?w=favicon&site=example.com
Et j’avais cette arborescence :

cache.php
var/
| - - cache/
      | - - favicon/
            | - - icone1.png
            | - - icone2.png
            | - - ...
      | - - gravatar/
            | - - avatar1.png
            | - - avatar2.png
            | - - ...
      | - - .htaccess

Quand je voulais une icône, je faisais un hit sur cache.php?get=icone1.png, et il m’envoyait l’icone1.png après l’avoir soit téléchargée, soit lue sur le disque.
Pour moi, l’image était située à l’URL cache.php?get=icone1.png, pas ailleurs.

Maintenant je fais autrement.

Je fais mon hit sur /var/cache/favicon/icone1.png. Plus besoin de PHP : si l’image existe, l’icône est envoyée directement.

Mais si le fichier n’existe pas, ça renvoie un 404, non ?

Exactement ! Mais ça, c’est uniquement si le fichier n’est pas là.
La distinction « fichier là / fichier pas là » n’est plus à faire par PHP comme avant : elle est déjà faite par le serveur (Apache, …), et on va s’en servir !

Il est possible d’utiliser une redirection en cas de 404. Dans le fichier .htaccess de notre arborescence, je vais mettre :

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule (.*) ../../cache.php?w=gravatar&q=$1 [L]

L’important ici est le !-f de la première ligne.

Un simple -f signifierait « notre requête est un fichier ». Mais avec le point d’exclamation devant, ça signifie « notre requête n’est pas un fichier », dans le sens « un fichier qui n’est pas, qui n’existe pas ».

Si je demande un fichier qui n’est pas là (donc un 404), cette condition est est satisfait : « le fichier n’est pas ! » et on accède à la ligne suivante, c’est à dire le renvoie vers le script PHP.

Ce n’est pas une redirection 301 ou 302 : le serveur demande uniquement à PHP de s’occuper de la demande au lieu de s’en occuper lui-même par l’envoie d’une erreur 404.

Une fois que PHP a fait son boulot, il sauvegarde le fichier et la renvoie au navigateur : le navigateur ne reçoit jamais de 404 : si le fichier est là, le serveur lui donne. Si le fichier n’est pas là, PHP produit le fichier avant de lui donner également. Le fichier est également sauvegardé pour la prochaine fois.

En plus de ça, la requête est faite directement sur le fichier que l’on veut, pas sur une page à PHP qui devra lire le fichier. On gagne donc en logique aussi.

Pour aller plus loin

Comme je l’ai dit, j’utilise ce système à la fois pour des icônes de site et pour des avatars de commentaires. Il y a donc une distinction à un moment. Les images sont mis en cache dans deux répertoires dédiés :

var/
| - - cache/
      | - - favicon/
      | - - gravatar/
      | - - .htaccess

Aussi, une complication est de ne faire qu’un seul fichier .htaccess.

Pourquoi ? Car je ne veux pas mettre un .htaccess dans chaque dossier et ceci pour une raison simple : quand veux purger le cache (en supprimant le dossier et son contenu), je ne veux pas perdre mon fichier .htaccess.

Avec un seul fichier situé à un niveau plus haut, je supprime les dossier favicon/ et gravatar/ et c’est bon, ils seront recréés par PHP lors de la prochaine requête.

Mon .htaccess doit distinguer quel est le dossier où l’on fait la requête. Je fais ça comme ça :

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} ./gravatar/(.*)$
RewriteRule (.*)gravatar/(.*)$ ../../favatar.php?w=gravatar&q=$2 [L]

Explication ligne par ligne :

RewriteCond %{REQUEST_FILENAME} !-f

↑ Je regarde si le fichier existe. Si oui, le fichier est envoyé au navigateur et ça s’arrête. Autrement on continue.

RewriteCond %{REQUEST_URI} ./favicon/(.*)$

↑ Je regarde sur la requête concerne un fichier dans /favicon

RewriteRule (.*)favicon/(.*)$ ../../favatar.php?w=favicon&q=$2 [L]

↑ Si oui et oui, on renvoie sur favatar.php?w=favicon&q=icone.png, et PHP fera son travail. Le [L] permet de dire qu’il s’agit de la dernière condition et que le traitement du .htaccess s’arrête.

Il reste une ligne à ajouter. En effet, si je fais une requête sur le dossier .favicon/, je ne veux pas que ça renvoie sur PHP. C’est un dossier, pas une image. Et même si je gère cette exception dans PHP pour plus de sécurité, il faut mieux mettre un garde-fou en plus.

Par conséquent, ça nous fait quatre lignes :

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} ./gravatar/(.*)$
RewriteCond %{REQUEST_URI} !./gravatar/$
RewriteRule (.*)gravatar/(.*)$ ../../favatar.php?w=gravatar&q=$2 [L]

La ligne ajoutée, la troisième, dit « si le fichier est autre que le dossier gravatar/, on applique la règle ». Dans le cas contraire, on laisse faire (et on ne renvoie pas vers PHP.

Ceci est bon pour les avatars.
Reste à faire la même chose pour les favicon. Il suffit de dupliquer tout ça :

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} ./gravatar/(.*)$
RewriteCond %{REQUEST_URI} !./gravatar/$
RewriteRule (.*)gravatar/(.*)$ ../../favatar.php?w=gravatar&q=$2 [L]

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} ./favicon/(.*)$
RewriteCond %{REQUEST_URI} !./favicon/$
RewriteRule (.*)favicon/(.*)$ ../../favatar.php?w=favicon&q=$2 [L]

Et voilà !

Exemple de fonctionnement

Juste pour résumer ce qui se passe :

Une requête sur ./gravatar/fichier.png sera traité par le premier bloc ci-dessus. Une requête sur ./favicon/fichier.png sera traité par le seconde bloc ci-dessus.

Dans les deux cas, si fichier.png existe, il est envoyé normalement au navigateur. Sinon, il est créé par PHP puis envoyé. Il n’y a pas de 404 envoyé au navigateur.
Si le fichier demandé est invalide, on renvoie une erreur 400 (Bad Request) avec PHP. C’est par exemple le cas si on demande un fichier à Gravatar qui n’est pas un MD5 d’une adresse e-mail, ou si l’on demande un favicon pour une URL qui n’est pas une URL correcte.

Enfin, si je fais une requête sur un autre dossier (./foo/fichier.png par exemple), alors il est ignoré par ce .htaccess et par PHP. Pas de risque donc de faire tourner PHP sur d’autres fichiers que des icônes ou des avatars.

image d’en-tête de Ferenc Almasi