PHP propose un mécanisme de bufferisation : toutes les sorties à destination du navigateur sont stockée dans un buffer (ou zone tampon). Tant que le script s'exécute, ou tant qu'aucune instruction de vidage du buffer n'est donnée, PHP met dans ce tampon ce qui est envoyé au navigateur. Quand le script est terminé, PHP remet en ordre le buffer pour que l'envoi soit conforme à un message HTTP. Si des en-têtes ont été envoyés avec la fonction header() ou des cookies avec setcookie() ils sont placés en début du message. Ensuite PHP placera toutes les sorties faites par le script avec echo, print(), etc.

Ce qui est intéressant si nous activons la bufferisation, c'est que nous pouvons agir sur le contenu du buffer avant qu'il ne soit finalement envoyé et y apporter des modifications, des remplacements, ou le sauvegarder pour être réutilisé plus tard (principe de cache).

Plusieurs fonctions permettent de gérer la bufferisation et en particulier :

ob_start() | démarre le buffering des sorties. Le buffering supporte plusieurs niveaux (ie plusieurs ob_start()).
ob_end_flush() | envoie le contenu du buffer et arrête le buffering.
ob_end_clean() | supprime le contenu du buffer sans l'envoyer, et arrête le buffering.
ob_get_contents() | retourne le contenu du buffer sous la forme d'une chaîne de caractères.

Si la bufferisation des sorties nous facilite la vie pour les redirections, les envois d'en-têtes et de cookie (comme le voyons un peu plus loin), cette technique peut être utilisée pour de nombreuses autres choses.

### Remplacer la sortie

La bufferisation nous permet d'empêcher la sortie normale, capturée dans le tampon, et d'envoyer à la place une autre sortie totalement différente.
Cette technique peut être illustrée par un exemple de gestion des messages d'erreur. A défaut d'éviter les erreurs, nous pouvons au moins éviter que les messages d'erreur polluent les pages, qu'ils s'affichent au milieu d'une page à moitié construite ou qu'ils donnent des indications pouvant être utilisées pour du piratage.

Exemple : Gestion d'erreur
<?php echo '<html lang="fr"><head><title>Ze matrix</title></head>'; echo '<body style="background: #000; color: #0f0">'; echo '<h1 style="text-align: center; '; echo 'border: 2px dotted;">ZE MATRIX</h1>'; echo '<h2>Le plan de la matrix</h2>'; readfile('zeplanofzezone'); echo '<h2>Saute dans le phone et rejoins moi au 2631.</h2>'; ?>

Pas très pro comme effet ...

Pour éviter ce genre de désagrément, nous pouvons en cas d'erreur, ne pas afficher une page composée d'éléments corrects et de messages abscons, mais afficher totalement autre chose.

Exemple : Gestion d'erreur
<?php ob_start(); // Démarre la bufférisation set_error_handler('gererErreurs'); function gererErreurs($num, $msg, $fichier, $lig) { ob_end_clean(); // supprime le contenu du buffer echo '<html lang="fr"><head><title>Ze matrix Error</title></head>', '<body style="background: #000; color: #0f0">', '<h2>Ici Neo : Erreur dans la matrice</h2>', '<p>Erreur ', $num, ' ligne ', $lig, ' dans ', basename($fichier), '<p>Send this code to Sion ...', '</body></html>'; exit(); } echo '<html lang="fr"><head><title>Ze matrix</title></head>'; echo '<body style="background: #000; color: #0f0">'; echo '<h1 style="text-align: center; '; echo 'border: 2px dotted;">ZE MATRIX</h1>'; echo '<h2>Le plan de la matrix</h2>'; readfile('zeplanofzezone'); echo '<h2>Saute dans le phone et rejoins moi au 2631.</h2>'; ?>

La première instruction est ob_start() qui demarre le processus de bufferisation. A partir de ce moment, tous les envois fait par le script au navigateur sont mis en attente dans un tampon.

La deuxième instruction set_error_handler() définit la fonction gererErreurs qui capturera les erreurs éventuelles dans le script.

Dans cette fonction gererErreurs, avec ob_end_clean() nous commençons par arrêter la bufferisation et effacer le tampon contenant le code HTML déjà généré . Ensuite, il suffit d'afficher un message expliquant qu'une regrettable erreur s'est produite ...

Dans un site réel, un tel message devrait être remplacé par quelque chose de plus sibyllin comme "Notre site fait face à un pic de trafic. Merci de ré-essayer plus tard.".

### Stocker les sorties

Ce qui est interessant avec la bufferisation des sorties, c'est que nous pouvons stocker la sortie et la réutiliser si elle est demandée à nouveau. Grâce à cette technique nous pouvons créer un cache pour des pages qui seraient appelées souvent et dont les informations ne doivent pas forcément être les toutes dernières, mais pour lequel un délai de rafraichissement est possible (1 heure, 1 jour, etc).

Comme exemple nous prendrons une page qui doit afficher la liste des communes d'un département. La liste de toutes les communes françaises est fournie par l'INSEE sous la forme d'un fichier texte (extension csv pour import dans excel) de 38949 lignes, composées de 4 champs : le nom de la commune, le code postal, le nom du département et le code INSEE de la commune.

Nous trouverons par exemple :
BESANCON;25000;DOUBS;25056
BETHONCOURT;25200;DOUBS;25057
BEURE;25720;DOUBS;25058
BEUTAL;25250;DOUBS;25059
BIANS LES USIERS;25520;DOUBS;25060
BIEF;25190;DOUBS;25061

L'utilisateur peut afficher toutes les communes du département qu'il choisit. Plutôt que rechercher la liste des communes d'un département à chaque demande, nous pouvons créer la liste des communes d'un département à sa première demande, puis utiliser cette page déjà créée pour les demandes suivantes (principe du cache).

Nous utilisons la fonction ob_get_contents() qui retourne le contenu du tampon sous la forme d'une chaîne de caractères pour écrire dans un fichier que nous pourrons ensuite afficher directement sans refaire le traitement de sélection des départements.

Remarque : l'exemple suivant utilise plusieurs instructions de lecture et d'écriture de fichier. Elles sont facilement compréhensible, et vous aurez tous les détails nécessaires dans le chapitre sur le traitement des répertoires et des fichiers.

Exemple : système de cache
<?php ob_start(); // Démarrage de la bufferisation require('bib_fonctions.php'); $titre = 'Communes d\'un département'; //------------------------------------------------------ // Phase d'affichage du formulaire de recherche //------------------------------------------------------ if (!isset($_POST['btnVoir'])) { htmlDebut($titre); afficherForm(''); htmlFin(); exit(); } //------------------------------------------------------ // Phase de soummission du formulaire de recherche //------------------------------------------------------ //------------------------------------------------------ // Vérification du numéro de département $dept = trim($_POST['txtDept']); if (! estEntier($dept) || ! estEntre($dept, 1, 99)) { htmlDebut($titre); afficherForm($dept, 'Numéro entre 1 et 99 SVP'); htmlFin(); exit(); } $fichierCache = 'depart_'.$dept.'.html'; //------------------------------------------------------ // Si le fichier cache existe et a été généré il y a // moins d'une heure on l'affiche et le traitement est fini. if (file_exists($fichierCache)) { $dateCache = filemtime($fichierCache); if (time() - $dateCache < 3600) { include($fichierCache); exit(); } } //------------------------------------------------------ // Il faut générer le fichier cache. On fait les affichages // tout à fait normalement. On les récupérera dans le buffer // à la fin du traitement htmlDebut($titre); afficherForm($dept); $insee = fopen('insee.csv','r'); //Ouvertue fichier INSEE while (! feof($insee) ) { // Boucle de lecture $enreg = fgets($insee); // lecture ligne $zones = explode(';', $enreg); if (substr($zones[1], 0, 2) == $dept) { echo $zones[0], ' (', $zones[1], ')<br>'; } } fclose($insee); // fermeture du fichier INSEE htmlFin(); //------------------------------------------------------ // On récupère le buffer de sortie et on l'enregistre // dans un fichier $sortie = ob_get_contents(); $fichier = fopen($fichierCache, 'w');// création du fichier fwrite($fichier, $sortie); // Ecriture du fichier fclose($fichier ); // Fermeture du fichier //------------------------------------------------------ // On envoie le buffer au navigateur car on doit dans // cette phase afficher le contenu généré ob_end_flush(); //------------------------------------------------------ /** * Affichage du formulaire de recherche * * @param string $dept numéro de département * @param string $err texte d'un message d'erreur */ function afficherForm($dept='', $err='') { if ($err != '') { htmlInfo($err); } echo '<form method="post" ', 'action="', $_SERVER['PHP_SELF'], '">', 'Numéro du département : ', '<input type="text" name="txtDept" size="4" ', 'value="', htmlentities($dept), '">', '<input type="submit" name="btnVoir" value="Voir">', '</form>'; } ?>

La première fois que l'on choisit un département particulier, un fichier html est créé avec la liste des communes de ce département. Par exemple si je choisis le département 25, le fichier depart_25.html sera créé. Ensuite, chaque fois que je demanderai le département 25, avant de faire le traitement de recherche des communes, le script regardera si le fichier depart_25.html existe, et si il a été créé il y a moins d'une heure, il affichera le contenu de ce fichier plutôt que de refaire le traitement. Dans cet exemple, le laps de temps d'un heure est bien entendu incohérent car il ne se créée pas une nouvelle commune en France toutes les heures. Dans un cas réel il faudrait le moduler selon la fréquence de mise à jour des informations traitées.

### Modifier les sorties

La fonction ob_start() accepte un paramètre optionnel : le nom d'une fonction à appeler automatiquement quand la bufferisation est terminée (fonction de callback). ob_start() passera le contenu du buffer à cette fonction qui pourra alors effectuer des traitements sur ce contenu avant qu'il ne soit envoyé au navigateur dans un message HTTP.

Grâce à elle nous pouvons par exemple compresser le code HTML envoyé au navigateur si celui-ci supporte la compression. Parmi les en-têtes HTTP que le navigateur passe au serveur lors d'une demande, Accept-Encoding définit quels algorithmes de compression le navigateur est capable de gérer. Il enverra par exemple :
Accept-Encoding: gzip,deflate

Avec la valeur de cet en-tête, nous saurons si nous pouvons envoyer du HTML compressé au navigateur. PHP nous fournit la fonction ob_gzhandler() qui va déterminer si le navigateur client supporte la compression gzip, et si oui va compresser le contenu à envoyer. La fonction s'utilise très simplement :
ob_start("ob_gzhandler") ;
Notez que le nom de la fonction doit être entouré de guillemets et ne comporter ni parenthèse ni paramètre.

Si au niveau du serveur, il faudra un peu de temps CPU pour compresser les données, on gagnera environ 40% du temps nécessaire à l'envoi.

Exemple : HTML compressé
<?php ob_start('ob_gzhandler'); require('bib_fonctions.php'); htmlDebut('HTML compressé'); echo 'Cette page est envoyée compressée au navigateur.'; echo 'A utiliser avec de gros fichiers pour réduire le ', 'trafic sur le réseau.'; htmlFin(); ?>