par Alexandre Alapetite le 2004-07-04 ; mise à jour 2009-08-16

Les requêtes HTTP conditionnelles en PHP

HTTP dispose d’un mécanisme permettant de tirer parti efficacement du système de cache et des autres capacités du navigateur client, pour économiser la bande passante, la puissance de calcul du serveur, et améliorer les temps de réponse.

Lorsqu’un client demande pour la première fois un document, celui-ci lui est transmis. Mais lorsque le client vient à redemander ce document, celui-ci ayant peut-être été mis à jour entre temps, le client fourni une date et un identifiant unique de la version dont il dispose ; le serveur transmettra à nouveau le document uniquement si celui-ci a été modifié depuis la version dont dispose le client, sinon le serveur enverra un avis de non-modification. Dans tous les cas, le client transmet aussi ses capacités, et la communication est optimisée selon ses paramètres, avec compression des données et connexions persistantes.

Résumé : Je propose une librairie libre — une seule fonction — qui permet de gérer les différents types de requêtes conditionnelles (304, 412), les requêtes HEAD, le cache au niveau client et proxys, la compression des données et les connexions persistantes. En mode RSS/ATOM, permet de filtrer les articles par date côté serveur pour n’envoyer au client que les nouveaux articles. Propose aussi un support basique des sessions. Aucune modification dans la configuration de PHP ou du serveur HTTP n’est requise. Aucun logiciel supplémentaire n’est nécessaire, ni côté serveur, ni côté client. Nécessite seulement l’inclusion au début du script PHP de la bibliothèque avec un require() et un appel de fonction.

Télécharger le module

English

Sommaire

Quitter

Fonctionnement du dialogue HTTP lors des requêtes conditionnelles

Les principaux entêtes HTTP en jeu

Last-Modified: Thu, 08 Jul 2004 17:33:54 GMT
Le serveur retourne la date de la dernière modification du document (ou de son contenu). Compatible HTTP/1.0. Le fait d’avoir une date en clair est intéressant pour les utilisateurs et les moteurs de recherches. Voir la référence pour le format des dates.
Etag: "82e81980-27f3-4a6ae480"
Le serveur retourne un identifiant unique du document envoyé, qui doit changer si l’adresse ou le contenu de ce document sont modifiés. Peut être utilisé en même temps que Last-Modified ou tout seul (HTTP/1.1). Le fait d’avoir un code crypté permet de masquer si on le souhaite la date de dernière modification. Voir la référence pour le format du ETag.
If-Modified-Since: Thu, 08 Jul 2004 17:33:54 GMT
Envoyé par le client pour demander une mise à jour si le document a été modifié depuis cette date. La date envoyée doit être la même chaîne de caractères que celle fournie par le serveur dans l’entête serveur Last-Modified lors de la dernière réception de ce document.
If-None-Match: "82e81980-27f3-4a6ae480"
Envoyé par le client pour demander une mise à jour si aucun document ne correspond plus à cet identifiant. Le ETag envoyé doit être le même que celui reçu dans l’entête serveur Etag lors de la dernière réception de ce document.
Cache-Control: private, max-age=0, must-revalidate
Utilisé par le serveur, les caches intermédiaires et le client pour gérer principalement le cache client et la manière dont le document peut être partagé par plusieurs utilisateurs sans avoir à interroger le serveur à chaque fois.
Accept-Encoding: gzip,deflate
Envoyé par le client pour informer le serveur que le document peut être transféré en utilisant un des formats modifiés (compressés) listés.
Content-Encoding: gzip
Envoyé par le serveur pour informer le client que le document est transféré dans un format modifié (compressé). Ce format devrait être un de ceux proposés par le client dans Accept-Encoding.
Content-Length: 3495
Si la longueur du document est connue (document statique, ou dynamique avec un tampon), ce champ est utilisé par le serveur pour préciser au client la taille du document. Cela permet les connexions persistantes ; sinon, lorsque la taille n’est pas connue, le transfert est effectué en mode “chunked” et la fin du document est marqué par la fermeture de la connexion par le serveur.
Connection: keep-alive
Utilisé par le serveur et le client pour transférer plusieurs documents avec la même connexion persistante. Afin de pouvoir faire cela, la plupart des serveurs et clients nécessitent que l’entête Content-Length soit renseigné.

Voir ma documentation sur les redirections Web en HTTP et HTML pour des commentaires sur les entêtes HTTP.


Schéma classique de communication

Dans l’exemple ci-dessous, seuls les entêtes HTTP utiles aux requêtes conditionnelles ont été mentionnés.

1. Le client demande une première fois un document

GET /test.php HTTP/1.1

2. Le serveur fourni le document

HTTP/1.x 200 OK
Date: Thu, 08 Jul 2004 17:42:26 GMT
Last-Modified: Thu, 08 Jul 2004 17:33:54 GMT
Etag: "82e81980-27f3-4a6ae480"

<html>
...
</html>

3. Le client demande une deuxième fois le document, et envoie les références de la version dont il dispose déjà (en cache)

GET /test.php HTTP/1.1
If-Modified-Since: Thu, 08 Jul 2004 17:33:54 GMT
If-None-Match: "82e81980-27f3-4a6ae480"

4. Le serveur retourne un avis de non-modification car le document n’a pas été modifié depuis le dernier passage du client

HTTP/1.x 304 Not Modified
Date: Thu, 08 Jul 2004 17:46:31 GMT
Etag: "82e81980-27f3-4a6ae480"

5. Le client demande une troisième fois le document

GET /test.php HTTP/1.1
If-Modified-Since: Thu, 08 Jul 2004 17:33:54 GMT
If-None-Match: "82e81980-27f3-4a6ae480"

6. Le serveur fourni la nouvelle version du document car celui-ci a été modifié

HTTP/1.x 200 OK
Date: Thu, 08 Jul 2004 17:48:54 GMT
Last-Modified: Thu, 08 Jul 2004 17:48:52 GMT
Etag: "82e81980-2bf2-7ff14900"

<html>
...
</html>

Répondre à une requête HTTP conditionnelle

Ce mécanisme est normalement pris en charge automatiquement par les serveurs HTTP (comme Apache, IIS, ...) pour les documents statiques, comme les pages HTML, les images JPEG, etc. mais il est laissé à la charge du programmeur pour les documents dynamiques générés en PHP, CGI, etc.

Aucun logiciel supplémentaire n’est nécessaire, ni côté client ni côté serveur. Les serveurs et les navigateurs les plus courants sont tous nativement compatibles avec cette technique. Dans le cas où le navigateur client ne serait pas compatible HTTP/1.1, l’utilisation de ces techniques n’a aucun effet secondaire et la communication se déroule normalement, mais sans optimisation.

Je propose un module à inclure en haut de vos pages PHP pour gérer automatiquement ces requêtes conditionnelles, et économiser ainsi du temps de calcul, de la bande passante, et fournir vos réponses plus rapidement. Il permet de gérer les caches au niveau du client et des proxys, et permet la compression des données. Les connexions persistantes sont permises, lorsque la compression des données est activée, selon la politique du serveur. Il y a aussi une fonctionnalité spéciale pour les fils RSS/Atom qui permet de ne transmettre au client que les nouveaux articles. Un support basique des sessions est inclus.

Ce module s’occupe des différents entêtes conditionnels, tient compte de la dernière modification du script lui-même, et gère les requêtes HEAD.


Comment utiliser cette librairie ?

Avant d’envoyer le moindre texte au client, il suffit d’appeler la fonction httpConditional() avec :

$UnixTimeStamp (obligatoire)
La date de dernière modification des données, au format timestamp UNIX (nombre de secondes depuis le 1er janvier 1970 à 00:00:00 GMT).
La fonction httpConditional() prend soin de vérifier si le script lui-même a été modifié.
$cacheSeconds=0 (facultatif)
Durée de vie en seconde de la version en cache du document. Si < 0, le cache est désactivé. Mettre à 0 pour demander la revalidation à chaque accès au document.
$cachePrivacy=0 (facultatif)
Politique de cache : niveau de partage entre différents utilisateurs des versions en cache. 0=privé, 1=normal (public), 2=public forcé (même lorsque la zone accédée requiert un mot de passe, ou afin d’activer le cache en HTTPS).
$feedMode=false (facultatif)
Spécial pour les fils RSS/ATOM : permet de donner au client uniquement les articles plus récents que son dernier passage.
Si ce paramètre est passé à vrai, la variable globale $clientCacheDate contiendra la date de la version dans le cache du client, la politique de cache est forcée automatiquement à privé, la connexion est fermée rapidement car le client ne prend en général qu’un seul fichier, et on ne tient pas compte de la date de modification du script.
$compression=false (facultatif)
Active la compression des données en tenant compte des capacités du client.
Permet les connexions persistantes, en fonction de la politique du serveur (ex: Apache les utilises, mais IIS ferme toujours).
Vous pouvez modifier le niveau de compression par défaut (6) avec zlib.output_compression_level dans php.ini ou avec ini_set('zlib.output_compression_level',7) par exemple (1..9).
$session=false (facultatif)
À activer quand les sessions sont utilisées.
Vérifie automatiquement si les données contenues dans $_SESSION ont été modifiées depuis l’appel à cette fonction lors de la dernière génération du document.
Utiliser avec session_cache_limiter(''); et/ou session.cache_limiter='' dans php.ini

Usage basique :

exemple.php

<?php
 require_once('http-conditional.php');
 //Date de la dernière modification du contenu (format Timestamp Unix)
 //Exemple: requête base de données
 $dateDerniereModification=...;
 if (httpConditional($dateDerniereModification))
 {//Aucune modification depuis le dernier passage du client
  ... //Ferme la base de données, et autres nettoyages
  exit(); //Pas besoin d’envoyer autre chose
 }
 //!\ Ne pas envoyer de texte au client avant cette ligne
 ... //La suite du script, comme s’il n’y avait pas cette première partie
?>
http-conditional.php

<?php
/*Optimisation : Permet le support des requêtes HTTP/1.x conditionnelles en PHP.*/

//En mode RSS/ATOM, contient la date de dernière mise à jour du client.
$clientCacheDate=0; //Variable globale publique car PHP4 ne permet pas les arguments optionels par référence
$_sessionMode=false; //Variable globale privée

function httpConditional($UnixTimeStamp,$cacheSeconds=0,$cachePrivacy=0,$feedMode=false,$compression=false,$session=false)
{//Credits: http://alexandre.alapetite.fr/doc-alex/php-http-304/
 //RFC2616 HTTP/1.1: http://www.w3.org/Protocols/rfc2616/rfc2616.html
 //RFC1945 HTTP/1.0: http://www.w3.org/Protocols/rfc1945/rfc1945.txt

 //Si les entêtes HTTP ont déjà été envoyés au client, trop tard, on ne peut rien faire.
 if (headers_sent()) return false;

 if (isset($_SERVER['SCRIPT_FILENAME'])) $scriptName=$_SERVER['SCRIPT_FILENAME'];
 elseif (isset($_SERVER['PATH_TRANSLATED'])) $scriptName=$_SERVER['PATH_TRANSLATED'];
 else return false;

 if ((!$feedMode)&&(($modifScript=filemtime($scriptName))>$UnixTimeStamp))
  $UnixTimeStamp=$modifScript; //Date de modification la plus récente entre les données et le script lui-même
 //La date de dernière modification ne doit pas être ultérieure à la date courante du serveur
 $UnixTimeStamp=min($UnixTimeStamp,time());

 //Si les conditions de la requête HTTP montrent que version présente dans le cache du navigateur client est bonne
 $is304=true;
 //Si les préconditions sont inacceptables
 $is412=false;
 //Pour une requête HTTP conditionnelle avec une réponse 304, il faut au moins une condition validée
 $nbCond=0;

 /*
 Format des dates: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
  # En HTTP/1.x, la date et l’heure doivent être dans le fuseau horaire de Greenwich (GMT).
  # Plusieurs recommandations on cohabitées:
   - HTTP/1.1 RFC2616 préfère le RFC822 mis à jour par RFC1123, compatible RFC733.
   - HTTP/1.0 se base sur la fonction Usenet getdate(3), RFC850 mis à jour par RFC1036,
  # Ces recommandations ont un dénominateur commun, plus strict, dans la zone horaire GMT, de la forme :
     Mon, 28 Jun 2004 18:31:54 GMT
  # D’où en PHP
     $httpDate=gmdate('D, d M Y H:i:s \G\M\T',$timeStamp);
 */
 $dateLastModif=gmdate('D, d M Y H:i:s \G\M\T',$UnixTimeStamp);
 $dateCacheClient='Tue, 10 Jan 1980 20:30:40 GMT';

 //Entity tag (Etag) de la ressource retournée.
 //http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
 //Doit changer si la ressource retourne un document différent, ou si le document a été modifié
 if (isset($_SERVER['QUERY_STRING'])) $myQuery='?'.$_SERVER['QUERY_STRING'];
 else $myQuery='';
 if ($session&&isset($_SESSION))
 {//Dans le cas de sessions, intègre les variables de $_SESSION dans le calcul du ETag
  global $_sessionMode;
  $_sessionMode=$session;
  $myQuery.=print_r($_SESSION,true).session_name().'='.session_id();
 }
 $etagServer='"'.md5($scriptName.$myQuery.'#'.$dateLastModif).'"'; //='"0123456789abcdef0123456789abcdef"'

 if ((!$is412)&&isset($_SERVER['HTTP_IF_MATCH']))
 {//http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24
  $etagsClient=stripslashes($_SERVER['HTTP_IF_MATCH']);
  //Comparaison du Etag actuel et de ceux du client
  $is412=(($etagClient!='*')&&(strpos($etagsClient,$etagServer)===false));
 }
 if ($is304&&isset($_SERVER['HTTP_IF_MODIFIED_SINCE']))
 {//http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25
  //http://www.w3.org/Protocols/rfc1945/rfc1945.txt
  //Récupère la date de la version dans le cache du navigateur client
  //Inutile de vérifier la consistance de la valeur, puisqu’on fera une comparaison de chaînes.
  $nbCond++;
  $dateCacheClient=$_SERVER['HTTP_IF_MODIFIED_SINCE'];
  $p=strpos($dateCacheClient,';'); //Internet Explorer ne respecte pas les standards
  if ($p!==false) //IE6 envoit quelque chose du style "Sat, 26 Feb 2005 20:57:12 GMT; length=134"
   $dateCacheClient=substr($dateCacheClient,0,$p); //Enlève l’information ajoutée par IE après la date
  //Comparaison des chaînes des dates du cache client et de la dernière modification
  //Doivent être identiques pour dire qu’il n’y a pas eu de modification (304 Not Modified)
  $is304=($dateCacheClient==$dateLastModif);
 }
 if ($is304&&isset($_SERVER['HTTP_IF_NONE_MATCH']))
 {//http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
  //Comparons les Etag pour vérifier que ce n’est pas cette version de ce document qui est en cache client
  $nbCond++;
  $etagClient=stripslashes($_SERVER['HTTP_IF_NONE_MATCH']);
  //Comparaison du Etag actuel et de celui du client
  $is304=(($etagClient==$etagServer)||($etagClient=='*'));
 }

 //$_SERVER['HTTP_IF_RANGE']
 //On ne gère pas les envois partiels. Retourne un 200 dans tous les cas.
 //http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.27

 if ((!$is412)&&isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE']))
 {//http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.28
  $dateCacheClient=$_SERVER['HTTP_IF_UNMODIFIED_SINCE'];
  $p=strpos($dateCacheClient,';');
  if ($p!==false)
   $dateCacheClient=substr($dateCacheClient,0,$p);
  $is412=(strcasecmp($dateCacheClient,$dateLastModif)!=0);
 }
 if ($feedMode)
 {//Spécial RSS
  global $clientCacheDate;
  $clientCacheDate=strtotime($dateCacheClient);
  $cachePrivacy=0;
 }

 if ($is412)
 {//http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.13
  header('HTTP/1.1 412 Precondition Failed');
  header('Content-Type: text/plain');
  header('Cache-Control: private, max-age=0, must-revalidate');
  echo "HTTP/1.1 Error 412 Precondition Failed: Precondition request failed positive evaluation\n";
  //La réponse est terminée, la requête du client est annulée
  //car le document a été changé depuis que le client a décidé une action
  //(évite un conflit d’édition par exemple)
  return true;
 }
 elseif ($is304&&($nbCond>0))
 {//http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
  header('HTTP/1.0 304 Not Modified');
  header('Etag: '.$etagServer);
  if ($feedMode) header('Connection: close'); //Vous devriez mettre cette ligne en commentaires si vous utilisez IIS
  return true; //La réponse est terminée, le client utilisera sa version en cache
 }
 else //La requête sera gérée de manière normale, sans condition
 {//http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.1
  //http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3
  if ($compression) ob_start('_httpConditionalCallBack'); //Utilise la compression des données
  //header('HTTP/1.0 200 OK'); //Géré par défaut par PHP
  //http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
  if ($cacheSeconds<0)
  {
   $cache='private, no-cache, no-store, must-revalidate';
   header('Pragma: no-cache');
  }
  else
  {
   if ($cacheSeconds==0) $cache='private, must-revalidate, ';
   elseif ($cachePrivacy==0) $cache='private, ';
   elseif ($cachePrivacy==2) $cache='public, ';
   else $cache='';
   $cache.='max-age='.floor($cacheSeconds);
  }
  header('Cache-Control: '.$cache);
  header('Last-Modified: '.$dateLastModif);
  header('Etag: '.$etagServer);
  //http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.10
  //Inutile de garder une connexion ouverte pour les fils RSS/ATOM
  //car la plupart du temps les clients ne vont prendre qu’un seul fichier
  if ($feedMode) header('Connection: close'); //Vous devriez mettre cette ligne en commentaires si vous utilisez IIS
  //http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
  //Dans le cas d’une requête HEAD,
  //on doit retourner les mêmes entêtes que dans le cas d’un GET,
  //mais le script n’a pas besoin de calculer le contenu
  return $_SERVER['REQUEST_METHOD']=='HEAD';
 }
}

function _httpConditionalCallBack(&$buffer,$mode=5)
{//Fonction privée appelée automatiquement à la fin du script lorsque la compression est activée
 //http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
 //Le niveau de compression peut être ajusté avec zlib.output_compression_level dans php.ini
 if (extension_loaded('zlib')&&(!ini_get('zlib.output_compression')))
 {
  $buffer2=ob_gzhandler($buffer,$mode); //Va vérifier HTTP_ACCEPT_ENCODING et mettre les entêtes appropriés
  if (strlen($buffer2)>1) //Quand ob_gzhandler réussit
   $buffer=$buffer2;
 }
 header('Content-Length: '.strlen($buffer)); //Permet les connexions persistantes
 //http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13
 return $buffer;
}

function httpConditionalRefresh($UnixTimeStamp)
{//Met à jour les entêtes HTTP si le contenu est vient d’être modifié par la requête du client
 //Voir un exemple sur http://alexandre.alapetite.fr/doc-alex/compteur/
 if (headers_sent()) return false;

 if (isset($_SERVER['SCRIPT_FILENAME'])) $scriptName=$_SERVER['SCRIPT_FILENAME'];
 elseif (isset($_SERVER['PATH_TRANSLATED'])) $scriptName=$_SERVER['PATH_TRANSLATED'];
 else return false;

 $dateLastModif=gmdate('D, d M Y H:i:s \G\M\T',$UnixTimeStamp);

 if (isset($_SERVER['QUERY_STRING'])) $myQuery='?'.$_SERVER['QUERY_STRING'];
 else $myQuery='';
 global $_sessionMode;
 if ($_sessionMode&&isset($_SESSION))
  $myQuery.=print_r($_SESSION,true).session_name().'='.session_id();
 $etagServer='"'.md5($scriptName.$myQuery.'#'.$dateLastModif).'"';

 header('Last-Modified: '.$dateLastModif);
 header('Etag: '.$etagServer);
}
?>

Utilisation recommandée

Avec $dateDerniereModification étant votre variable contenant la date de dernière modification des données au format UNIX, voici les arguments recommandés à passer à la fonction dans différents cas :

Document public classique
httpConditional($dateDerniereModification,18000,1,false,true)
Cache public de 5 heures, avec compression et connexions persistantes.
Document privé classique
httpConditional($dateDerniereModification,3600,0,false,true)
Cache privé de 1 heure, avec compression et connexions persistantes.
Document privé qui peut être modifié par l’utilisateur
httpConditional($dateDerniereModification,5,0,false,true)
Cache privé de 5 secondes, avec compression et connexions persistantes.
Fil RSS/ATOM
httpConditional($dateDerniereModification,3600,0,true,true)
Cache privé de 1 heure, mode RSS/ATOM avec fermeture rapide des connexions, avec compression.
Utilisez la variable globale $clientCacheDate pour filtrer par date les articles dans la requête SQL.

Fonctions de date utiles

Quelques fonctions PHP utiles pour gérer les dates, et les transmettre à httpConditional() au format UNIX :

time()
Retourne le timestamp UNIX actuel
strtotime()
Transforme un texte en timestamp UNIX
getlastmod()
Retourne la date de dernière modification de la page
filemtime()
Retourne la date de dernière modification du fichier passé en argument
MySQL UNIX_TIMESTAMP()
Dans une requête SQL pour MySQL, converti un champ date en un timestamp UNIX

Exemples d’utilisation

Ce module a été testé par exemple avec PHP/4.3.1/4.3.8/5.0.0 sous Apache/1.3/2.0.50 et IIS/5.1. Tous les clients ne prennent pas en charge l’ensemble des optimisations proposées, mais la communication a eu lieu sans problème avec tous les clients testés, comme InternetExplorer/5.0/5.5/6.0, Netscape/1.22/2.02/3.04/4.8/Mozilla, Opera/7, SharpReader/0.9.5, RSSreader/1.0.88...

Voici maintenant quelques exemples utilisant cette bibliothèque.

Cas d’un article avec MySQL

Pour tirer pleinement parti des avantages des requêtes conditionnelles, il faut que la date de dernière modification soit facilement et rapidement accessible. Aussi, une optimisation de la base de données dans ce sens est parfois souhaitable. On peut par exemple disposer d’une table contenant les principales dates de dernières modifications nécessaires.

Cas simple d’une page PHP avec un article issu d’une base de données MySQL.
La table des articles possède un champ “modified” contenant une date au format MySQL. On veut retourner par SQL la date de modification de cet article au format timestamp UNIX.

article.php

<?php
if isset($_GET['id']) $num=$_GET['id']; //Numéro de l’article demandé
else $num=0;
if (($connect=mysql_connect('server','user','password'))&&mysql_select_db('mybase'))
{
 $query='SELECT UNIX_TIMESTAMP(ar.modified) AS lastmod FROM articles ar WHERE ar.id='.$num;
 if (($result=mysql_query($query))&&($row=mysql_fetch_assoc($result)))
 {
  $dateLastModification=$row['lastmod'];
  mysql_free_result($result);
  if (httpConditional($dateLastModification))
  {//Aucune modification depuis le dernier passage du client
   mysql_close($connect);
   exit();
  }
 }
}
else $connect=false;
?>
<html>
...
<?php
if ($connect)
{
 ... //Autres requêtes vers la base de données
 mysql_close($connect);
}
?>
...
</html>

Dans cet exemple, nous avons laissé les paramètres par défaut, qui ont une politique de cache privée, avec une durée de vie de 0. Si cet article est public, sans condition d’accès particulière, on peut gagner des ressources en activant le cache et avec une politique publique, comme nous allons le voir dans le cas de l’image PNG suivante.


Image PNG dynamique

Dans ce cas-ci, nous devons générer une image PNG dynamique. Il s’agit d’une étiquette dont le titre provient d’un fichier texte label.txt séparé. La date de dernière mise à jour de cette image est donc celle de la dernière modification du fichier label.txt contenant les données.

Cette image publique est très accédée. On souhaite qu’une copie soit conservée dans le cache du client et les caches intermédiaires (proxy, fournisseurs d’accès, etc.) afin de décharger le serveur. On choisit une durée de vie du cache de 180 secondes. C’est un compromis à faire entre la charge serveur et la fraîcheur de l’information, selon la fréquence des modifications de cette image.

image.png.php

<?php
require_once('http-conditional.php');
header('Content-type: image/png');
//Date de modification du fichier contenant le texte de l’étiquette
$dateLastModification=filemtime('label.txt');
if (httpConditional($dateLastModification,180,2)) //Cache public, 180 secondes
 exit(); //Aucune modification depuis le dernier passage du client
if ($handle=fopen('label.txt','r')) //Ce fichier contient “Hello World!”
{
 $label=fread($handle,255); //Lecture du texte de l’étiquette
 fclose($handle);
}
else $label='Error';
$im=@imagecreate(120,30) or die('GD library error');
$bgcolor=imagecolorallocate($im,160,150,255);
$color=imagecolorallocate($im,5,5,5);
imagestring($im,5,7,7,$label,$color);
imagepng($im);
imagedestroy($im);
?>

Cas d’un fil RSS avec ODBC

Dans ce cas-ci, on veut savoir par SQL la date de modification la plus récente d’éléments situés dans plusieurs tables.
Ce fil RSS1.0 est composé des modifications les plus récentes des tables “articles”, “breves”, “documents”. La date de dernière mise à jour est donc celle de la de plus récente modification d’une de ces tables. Chaque table dispose d’un champ “modified” contenant une date timestamp UNIX.

Afin d’éviter de retransmettre l’ensemble des articles du fichier RSS a chaque passage du client, nous allons filtrer les articles par date, et ne donner au client que les articles postérieurs à son dernier passage ($clientCacheDate). Cela procure un gain important en bande passante. Ceci implique que la réponse est personnalisée en fonction du client, et que la mise en cache doit être privée ($cachePrivacy=0). Ceci est vérifié automatiquement par la fonction lorsque $feedMode est mis à vrai.
De plus, nous allons activer la compression des données si le client la gère ($compression=true), ce qui permettra de diminuer la taille du texte envoyé.
Si aucun nouvel article n’est disponible, un code de non-modification 304 sera envoyé au client, comme dans les exemples précédant.

Le support HTTP/1.1 et de la compression dans les aggrégateurs RSS (clients) est moins fréquent que dans les cas des navigateurs Internet, mais de plus en plus permettent d’utiliser cette optimisation, comme SharpReader. Là encore, si le client ne supporte pas HTTP/1.1, l’optimisation est perdue, mais la communication se déroule normalement, sans effet secondaire.

rss.php

<?php
require_once('http-conditional.php');
header('Content-Type: application/rss+xml');
if ($odbc=odbc_connect('basetest','user','password'))
{
 $sql='SELECT MAX(modif) AS lastmod FROM ('.
  'SELECT MAX(ar.date) AS modif FROM articles ar UNION '.
  'SELECT MAX(br.date) AS modif FROM breves br UNION '.
  'SELECT MAX(dc.date) AS modif FROM documents dc)';
 if (($query=odbc_exec($odbc,$sql))&&odbc_fetch_row($query))
 {
  $dateLastModification=odbc_result($query,'lastmod');
  odbc_free_result($query);
  if (httpConditional($dateLastModification,1800,0,true,true)) //Cache privé, 30 minutes et compression activée
  {//Aucune modification depuis le dernier passage du client
   odbc_close($odbc);
   exit();
  }
  //La variable globale $clientCacheDate contient maintenant la date du dernier passage du client
 }
}
echo '<','?xml version="1.0" encoding="UTF-8"?',">\n";
?>
<rdf:RDF xmlns="http://purl.org/rss/1.0/" xml:lang="en-GB"
 xmlns:dc="http://purl.org/dc/elements/1.1/"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">
  <channel rdf:about="http://alexandre.alapetite.fr/blog/actu.en.html">
    <title>My RSS</title>
    <description>Description of my RSS</description>
    <link>http://alexandre.alapetite.fr/</link>
    <dc:language>en-GB</dc:language>
    <dc:date>2004-07-22</dc:date>
    <dc:creator>Alexandre Alapetite</dc:creator>
    <items>
      <rdf:Seq>
<?php
if ($odbc)
{
 /*
  Filtre les articles :
  - Uniquement les articles plus récents que $clientCacheDate (le dernier passage du client)
  - Articles âgés de 30 jours au maximum
  - Au maximum les 20 articles les plus récents.
 */
 $clientDate=max($clientCacheDate-60,time()-(30*86400));
 $sql='SELECT TOP 20 * FROM ('.
  'SELECT title,link,date,description FROM articles WHERE date>'.$clientDate.
  ' UNION SELECT title,link,date,description FROM breves WHERE date>'.$clientDate.
  ' UNION SELECT title,link,date,description FROM documents WHERE date>'.$clientDate.
  ') ORDER BY date DESC';
 if (($query=odbc_exec($odbc,$sql))&&odbc_fetch_row($query,1))
 {
  odbc_fetch_row($query,0);
  while (odbc_fetch_row($query))
   echo "\t\t\t\t".'<rdf:li rdf:resource="http://alexandre.alapetite.fr'.odbc_result($query,'link').'"/>'."\n";
 }
}
?>
      </rdf:Seq>
    </items>
  </channel>

<?php
if ($odbc&&$query)
{
 odbc_fetch_row($query,0);
 while (odbc_fetch_row($query))
   echo "\t\t".'<item rdf:about="http://alexandre.alapetite.fr'.odbc_result($query,'link').'">'."\n",
    "\t\t\t".'<title>'.odbc_result($query,'title').'</title>'."\n",
    "\t\t\t".'<link>'.odbc_result($query,'link').'</link>'."\n",
    "\t\t\t".'<date>'.substr(date('Y-m-d\TH:i:sO',$myDate=odbc_result($query,'date')),0,22).':'.substr(date('O',$myDate),3).'</date>'."\n",
    "\t\t\t".'<description><![CDATA['.odbc_result($query,'description').']]></description>'."\n",
    "\t\t".'</item>'."\n";
 odbc_close($odbc);
}
?>
</rdf:RDF>

Utilisation avec des sessions

Cette librairie peut être utilisée conjointement aux sessions, en activant le paramètre session. Elle vérifie alors automatiquement si les données contenues dans $_SESSION ont été modifiées depuis l’appel à cette fonction lors de la dernière génération du document, en utilisant un hachage MD5 stocké dans l’entête HTTP ETag. Les trois cas de modifications détectées sont :

  1. si $_SESSION a été modifié après l’appel à httpConditional() lors de la dernière génération du document
  2. si $_SESSION a été modifié à partir d’un autre document
  3. si $_SESSION a été modifié avant l’appel à httpConditional() dans l’exécution courante

Il faut penser à désactiver la génération automatique d’entêtes des sessions avec session_cache_limiter(''); et/ou session.cache_limiter='' dans php.ini

session.php
<?php
 session_cache_limiter(''); //Désactive la génération automatique d’entêtes par la session
 session_start(); //Démarre la session
 ...
 require_once('http-conditional.php');
 //Date de la dernière modification du contenu (format Timestamp Unix)
 //Exemple: requête base de données
 $dateDerniereModification=...;
 if (httpConditional($dateDerniereModification,0,0,false,true,true))
 {//Aucune modification depuis le dernier passage du client
  ... //Ferme la base de données, et autres nettoyages
  exit(); //Pas besoin d’envoyer autre chose
 }
 //!\ Ne pas envoyer de texte au client avant cette ligne
 ... //La suite du script, comme s’il n’y avait pas cette première partie
?>

Compteur de visites

Voir l’exemple de compteur de visites en PHP/HTTP sur sa page dédiée.


Historique

1.6.2 2008-03-06
Correction et modification du comportement : lorsque $cacheSeconds==0 (valeur par défaut) une revalidation conditionnelle auprès du serveur est exigée à chaque accès au document ; lorsque $cacheSeconds<0 la mise en cache est désactivée.
1.6.1 2005-04-03
Support basique des sessions
1.5 2005-04-01
Génération de l’entête Content-Length lorsque le paramètre compression est activé, et ce même pour les navigateurs ne supportant pas la compression.
Correction : gestion du cas où le navigateur envoi un entête Accept-Encoding ne contenant ni gzip ni deflate.
1.4 2005-02-27
Ajout : fonction httpConditionalRefresh() permettant de mettre à jour les entêtes HTTP si le contenu est modifié par la requête du client
Correction : support d’Internet Explorer qui ne respecte pas les standards pour la valeur de If-Modified-Since
1.3.1 2004-11-23
Licence Creative Commons version française "CC BY-SA (FR)"
1.3 2004-08-05
Ajouts : mode RSS/Atom, compression et connexions persistantes
1.0 2004-07-26
Distribution initiale

Licence

Ce contenu est protégé par une licence Creative Commons Paternité - Partage des Conditions Initiales à l’Identique 2.0 France “BY-SA (FR)” [Creative Commons License]


Remerciements


Commentaires

object : Voir les commentaires

http://alexandre.alapetite.fr

Retour