Utilisation du tampon de sortie en PHP

Image non disponible

Lors du déclenchement d'un affichage en PHP (echo, var_dump, printf ou toute autre fonction), la chaine à afficher ne part pas directement vers l'affichage.
Elle est en réalité stockée dans différentes piles appelées "tampons", sur lesquelles l'utilisateur a un contrôle plus ou moins fin.
Lorsque le dernier tampon tout en bas est vidé, l'affichage est alors envoyé à un endroit, en fonction de la SAPI utilisée. Par exemple pour CLI, il s'agira de la sortie standard: la chaine est affichée à l'écran.
Nous allons ici détailler les différentes couches de tampon, leur utilisation et leur impact sur le code PHP.
2 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Présentation du système de tampon

I-A. Les différentes couches de tampon

PHP dispose de 2 couches de tampons, dont une dite "utilisateur" (sur laquelle l'utilisateur possède beaucoup d'options), celle-ci se situe en haut de la pile.
Vient ensuite le tampon de sortie, le fameux "output_buffering" que l'on configure via un paramètre dans php.ini, c'est la couche basse de PHP
Une fois cette couche traversée, PHP va écrire dans la SAPI, or celle-ci aussi peut être bufferisée. PHP vous offre alors la possibilité éventuelle de la vider ("flush").

Image non disponible
Les couches de tampon de sortie en PHP

I-B. Le tampon de sortie

Le tampon de sortie, appelé "output_buffering", met en tampon tout ce qui part vers l'affichage (vers la sortie standard) depuis PHP. Cette couche est configurable via le paramètre "output_buffering" de php.ini.
Ce paramètre peut prendre les valeurs "on", "off" ou encore un entier représentant la taille du tampon en octets. Si "on" est choisie, la taille de 40960 octets (40Ko) est utilisée.

Comme on peut s'y attendre, si la valeur "off" est utilisée, toute sortie PHP sera directement liée à l'entrée de la SAPI. Ceci n'est pas recommandé si la SAPI Apache est utilisée (mode web le plus courant) car de petites écritures répétées peuvent alors affecter les performances. La même recommandation est donnée pour CGI.

La SAPI CLI (mode ligne de commandes) désactive systématiquement le tampon PHP, quelle que soit la valeur donnée dans php.ini.
Ceci est plutôt logique, car en mode ligne de commande, on s'attend à ce que tout affichage déclenché en PHP soit immédiatement envoyé à l'écran.

I-B-1. Exemple

Cet exemple utilise la SAPI Apache (et ne fonctionnera pas en CLI).
Considérons que nous voulons afficher 3 caractères à la fois. Un caractère pèse un octet (par défaut, sans Unicode), il s'agit donc de régler output_buffering à 3 dans php.ini (ou au moyen de php_value dans .htaccess).

Considérant output_buffering=3 dans php.ini
Sélectionnez
<?php
echo 'aa'; // rien ne s'affiche à l'écran
sleep(3);
echo 'b'; // 3ème octet envoyé sur la sortie, le buffer est vidé et la chaine "aab" est affichée à l'écran
sleep(3);
echo 'c'; // rien ne s'affiche
sleep(3);

// le script se termine, PHP vide son buffer et 'c' est affiché à la suite
?>

Relativement simple.
Une valeur de 40960 (comme celle par défaut) n'est pas mauvaise dans un cas web réel. Gardez en tête qu'il vaut mieux que PHP envoie tout d'un coup vers la SAPI plutôt que morceaux par morceaux, pour des raisons de performances.

Si vous n'obtenez pas ces résultats à l'écran et que tout s'affiche d'un coup, c'est tout simplement qu'il reste encore un tampon: celui de la SAPI. Il fait l'objet du chapitre suivant.
Il est important de garder en tête que d'autres tampons peuvent encore intervenir, et ceux-là, vous n'y avez pas accès au moyen de PHP. On pourrait parler par exemple du tampon réseau (TCP buffer), ou encore d'un ou plusieurs tampons dans le noyau de l'OS. Enfin le client web, le navigateur, peut lui aussi recevoir des infos mais ne pas déclencher l'affichage immédiatement: Là encore, PHP n'a rien à voir.

I-B-2. Les en-têtes HTTP

PHP est un langage orienté web. Ainsi, le contexte HTTP est très souvent utilisé et nécessite l'envoi des en-têtes HTTP avant le contenu. Ainsi, vous devez appeler des fonctions comme header(), session_start() ou encore setcookie() avant tout déclenchement d'affichage. Cependant, si vous activez le buffer de sortie de PHP, celui-ci va non seulement stocker le contenu de la sortie, mais aussi les en-têtes que vous envoyez. Lors de sa vidange, il va lui-même envoyer d'abord les en-têtes, puis le contenu. Il va donc vous autoriser ce genre de fantaisie:

Le tampon de sortie envoie d'abord les en-têtes HTTP puis le contenu
Sélectionnez
<?php
/*
Nous supposons que la taille du tampon de sortie est supérieure à
9 octets (nous allons afficher 9 octets)
*/

echo "foo";
session_start();
echo "bar";
header("X-Some-Header: foobar");
echo "baz";

Faire des entorses à HTTP est peu recommandé, mais si on s'y connait bien on peut utiliser le tampon à notre avantage pour résoudre certaines problématiques.
Attention, dès que le tampon est vidé, PHP envoie les en-têtes HTTP et on ne pourra donc plus en envoyer par la suite.

Supposant un output_buffering=3
Sélectionnez
<?php
echo 'aa';
header('Foo: bar');
sleep(3);
echo 'b'; // Vide le tampon car il fait 3 octets
header('Bar: baz'); // Warning: Cannot modify header information - headers already sent by ...

Les en-têtes HTTP font partie de la sortie standard, ils empruntent donc le même canal que la texte affiché. La couche de tampon de PHP va simplement les séparer en interne, mais lors de l'envoi, en-têtes et contenu textuel seront envoyés dans le même tuyau (stdout).

I-C. Le tampon de la SAPI

Sous le tampon PHP se trouve le tampon de la SAPI. En effet, lorsque PHP vide son buffer, il le vide dans la SAPI qui elle aussi possède un tampon dont la taille n'est pas configurable via PHP.
Ce tampon-là peut encore retenir les informations avant de les envoyer réellement vers l'affichage (ou le client web, ou plus précisément la pile logicielle du dessous), c'est souvent le cas pour Apache (quelques octets).

Là où PHP va pouvoir prendre la main, c'est sur sa vidange: il est possible de demander à PHP de demander à la couche SAPI de vider ("flusher") son tampon. Ceci se fait de plusieurs manières en PHP:

  1. manuellement, en appelant la fonction PHP flush() ;
  2. systématiquement, en appelant la fonction PHP ob_implicit_flush() ;
  3. systématiquement, en indiquant dans php.ini implicit_flush=1.

Ainsi, si Apache vous gène en gardant dans son buffer des informations, vous pouvez corriger les scripts vus dans le chapitre précédant de plusieurs manières:

Considérant output_buffering=3 dans php.ini, vidange manuelle du tampon de SAPI
Sélectionnez
<?php
echo 'aa';
sleep(3); // rien ne s'affiche à l'écran
echo 'b'; // 3ème octet envoyé sur la sortie, le buffer est vidé dans la SAPI
flush(); // Demande à la SAPI de vider son buffer, si aucun autre tampon n'intervient, la chaine "aab" va s'afficher
sleep(3);
echo 'c'; // rien ne s'affiche
sleep(3);
?>
// le script se termine, PHP vide son buffer ET vide aussi celui de la SAPI. 'c' s'affiche donc à la suite
Considérant output_buffering=3 dans php.ini, vidange automatique du tampon de SAPI
Sélectionnez
<?php
ob_implicit_flush(1); // Dès que le buffer de la SAPI reçoit des données, demande sa vidange immédiate
echo 'aa';
sleep(3); // rien ne s'affiche à l'écran
echo 'b'; // 3ème octet envoyé sur la sortie, le buffer est vidé dans la SAPI qui se vide automatiquement

// etc.
Considérant output_buffering=3 ET implicit_flush=1 dans php.ini
Sélectionnez
<?php
// implicit_flush est à 1 dans php.ini, dès que le buffer de la SAPI reçoit des données, sa vidange est immédiate
echo 'aa';
sleep(3); // rien ne s'affiche à l'écran
echo 'b'; // 3ème octet envoyé sur la sortie, le buffer est vidé dans la SAPI qui se vide automatiquement

// etc.

Dès qu'on a compris comment les couches sont empilées et comment prendre la main dessus, tout est plus clair non?

Une fois de plus, la SAPI CLI (ligne de commande) fait parler d'elle. Non seulement elle impose output_buffering=0, mais en plus elle impose implicit_flush à 1. Logique une fois de plus, tout affichage PHP en ligne de commande est immédiatement envoyé vers la console (fflush() est appelée, pour les connaisseurs).

Pour plus d'informations sur le fonctionnement des SAPI, lisez SAPI et modes de communication.

II. Utiliser les tampons utilisateur

Au dessus du tampon PHP se trouvent les tampons utilisateur. Le développeur a pleinement la main dessus et peut en empiler autant qu'il souhaite.
Les tampons utilisateur se pilotent principalement au moyen des fonctions PHP ob_*(), le manuel officiel se situant icioutput buffer.

La taille des tampons est configurable, mais les tampons sont comme tout: des ressources, et à ce titre ils consomment de la mémoire. Plus vous utilisez de couches de tampon, à fortiori de grosse taille, plus la consommation mémoire de PHP augmentera.
En général, on en manipule un seul (si on utilise cette fonctionnalité) ou 2, rarement plus.

Voyons un exemple concret d'un tampon utilisateur.

Pour plus de "réalisme", vous comprendrez maintenant pourquoi je suggère pour les exemples de ce chapitre de mettre output_buffering = off et implicit_flush = 1 non?
Je veux tout simplement que tout ce qui va sortir à un moment donné de mes tampons utilisateur soit directement envoyé sur la sortie, sans passer par la couche tampon de PHP ni tampon de la SAPI.
Si vous voulez vous simplifier la vie, testez ces exemples sur la SAPI CLI qui possède déjà les bonnes valeurs de ces 2 directives (forcées). Utilisez le mode interactif (php -a) pour plus de réactivité.

Exemple très simple de tampon utilisateur
Sélectionnez
<?php
ob_start();
echo "hello world"; // rien ne s'affiche
?>
// Le script se termine, et "hello world" s'affiche

En fait, avec output_buffering = 0 et implicit_flush = 1, le fait de démarrer un tampon utilisateur avec ob_start() vous laisse le contrôle total sur ce tampon.
Pour configurer sa taille (6144 octets par défaut), utilisez le deuxième paramètre de ob_start():

Exemple très simple de tampon utilisateur d'une taille de 3 octets
Sélectionnez
<?php
ob_start(null, 3); // 3 octets de tampon
echo "fo"; // rien ne s'affiche, le tampon n'est pas plein

echo "o"; // le tampon est plein et se vide, "foo" s'affiche

/*
Cette chaine est supérieure à 3 octets, elle entre et ressort
tout de suite du tampon, elle est donc affichée instantanément
*/
echo "hello world"; 

Il est possible d'empiler plusieurs tampons qui seront alors "pipés" : dès que celui du dessus se vide, il remplit celui du dessous, et ainsi de suite. On peut donc obtenir des résultats rigolos, voici un exemple :

Empiler plusieurs tampons
Sélectionnez
<?php
ob_start(null, 10); // tampon 1, en bas de pile, taille 10 octets
ob_start(null, 8); // tampon 2, en haut de pile, taille 8 octets

echo "hello"; // remplit le tampon 2 de 5 octets (5/8)

echo "wo"; // rajoute 2 octets dans le tampon 2 (7/8)

/*
rajoute 2 octets dans le tampon 2 (9/8), le tampon est vidé dans celui du dessous, tampon 1 est à 9/10
donc toujours rien n'est affiché. Tampon 2 est à 0/8
*/
echo "rl"; 

/*
rajoute 10 octets dans tampon 2 (10/8), le tampon est vidé dans celui du dessous
tampon 1 est rempli par 10 octets, il est donc à 19/10 et se vide, comme plus rien
n'est en dessous, la chaine "hello world, comment" s'affiche
*/
echo "d, comment";

echo "ca va?" // rajoute 6 octets dans tampon 2 (6/8)

?>
/*
Le script se termine, PHP dépile les tampons et ce qui reste fini par s'afficher: "ca va?"
*/

On ne peut QUE remplir le tampon du haut de la pile, tous ceux plus bas sont intouchables tant que celui du haut n'est pas détruit (on va y venir).
On ne peut toucher à l'ordre de la pile, mais on peut prendre connaissance de son état (on va y venir).

II-A. Jouer avec le contenu des tampons

Les fonctions de PHP concernant les tampons utilisateur permettent pas mal de fantaisies :

Manipuler le contenu des tampons
Sélectionnez
<?php
ob_start(null, 20); // tampon 1
ob_start(null, 8); // tampon 2

/*
ob_get_level() retourne le nombre de tampons actifs empilés,
dans notre cas: 2
*/
printf("%d: dvp", ob_get_level());

// A ce stade, tampon 2 contient "2: dvp" (6/8)

/*
récupère le contenu du tampon courant (haut de pile, tampon 2)
sans pour autant le vider, ni le détruire
*/
$dvp = ob_get_contents(); // $dvp contient "2: dvp"

echo "\nfoo"; // Le tampon actif déborde (10/8) et se vide dans le tampon du dessous

echo "barbaz"; // tampon 2 est à 6/8, tampon 1 toujours à 10/20

/*
détruit le tampon de haut de pile (tampon 2) en détruisant aussi
son contenu qui est alors perdu
*/
ob_end_clean();

/*
Ici il n'y a plus qu'un tampon actif, le tampon 1, tampon 2 a été détruit
et la mémoire qu'il consommait (6 octets) a été libérée
*/

$foo = ob_get_clean(); // récupère le contenu du tampon courant et détruit le du tampon (0/20)

echo "booboo";

ob_end_flush(); // Vidange le tampon courant et détruit le, "2: dvp\nfoobooboo" est perdu définitivement

// A ce stade, plus aucun tampon utilisateur n'existe

Comme vous pouvez le noter, on a un contrôle fin sur les tampons, mais les fonctions ne donnent toujours accès qu'au tampon de plus haut niveau.
On peut encore remettre à zéro le contenu du tampon courant (donc supprimer son contenu) avec ob_clean(), le vidanger (le purger vers le buffer au niveau juste en dessous) avec ob_flush(), ou encore obtenir la taille de son contenu courant (en octets) au moyen de ob_get_length().

Si vous le souhaitez, vous pouvez verrouiller un tampon, c'est à dire empêcher toute manipulation de suppression ou de vidange. Pour cela, passez false au troisième paramètre de ob_start() ("erase"):

Verrouillage d'un tampon contre la vidange ou la suppression
Sélectionnez
<?php
ob_start(null, 1024, false);
echo "**du contenu ici** : on peut y aller, 1024 caractères sont autorisés avant vidange";

ob_flush(); // Erreur de type Notice
ob_clean(); // Idem

/*
Et idem pour ob_end_clean(), ob_end_flush(), vous ne pouvez *plus du tout*
supprimer ce tampon, vous êtes bloqué à ce niveau et ne pouvez descendre plus
bas.
*/

Attention: TOUT contenu envoyé vers l'affichage est mis en tampon. Une notice PHP, si display_errors est à On dans php.ini, va non seulement s'afficher en évitant tous les tampons, mais en plus aller se loger dans le tampon courant (et peut le faire déborder).
Ce choix d'implémentation est important à noter (les développeurs de PHP ont considérés qu'une erreur devait absolument être affichée quel que soit le nombre de tampons actifs sur le moment).
Il reste possible si on ne veut pas du tout afficher l'erreur, de rediriger display_errors sur stderr ou de mettre le paramètre à off tout simplement.

Attention aux erreurs (non fatales) de PHP!
Sélectionnez
<?php
ob_start(); // tampon de 6144 octets par défaut
echo $a; // Une erreur manifeste de type "Notice"

/*
Le message d'erreur s'affiche à l'écran en évitant tous les tampons
mais il a tout de même été stocké dans le tampon courant

PHP Notice:  Undefined variable: a in php shell code on line 1
PHP Stack trace:
PHP   1. {main}() php shell code:0
*/

$error = ob_get_contents();
ob_end_clean(); // Détruit le tampon courant et son contenu

echo $error;
/*
L'erreur précédente s'affiche ici, elle était bien stockée dans
le tampon et a été récupérée dans $error
*/

II-B. Les callbacks de buffer

Dans notre utilisation précédente de la fonction ob_start(), nous avions toujours passé son premier paramètre à null. Celui-ci permet d'indiquer une fonction de callback PHP à exécuter une fois la vidange ou le nettoyage du tampon.
La fonction reçoit comme paramètre une chaîne représentant le contenu du tampon courant ainsi qu'une constante d'état, et elle doit renvoyer une chaîne de caractères. Si la fonction retourne false, alors le tampon n'est pas traité et est envoyé, comme si la callback était transparente (elle aurait tout aussi pu renvoyer le contenu qu'elle a reçu).

Exemple de fonction de callback sur un tampon utilisateur
Sélectionnez
<?php
function dvp_callback($bufferContent)
{
    return sprintf('<div class="content">%s</div>', $bufferContent);
}

ob_start('dvp_callback');
echo "hello world";

/*
Le script PHP se termine, les tampons sont vidés et la callback est exécutée.
"<div class="content">hello world</div>" s'affiche
*/

Comme on peut s'y attendre, la fonction de callback doit exister et doit être invocable, ne doit pas lever d'exception, ne doit pas déclencher d'affichage et ne doit pas piloter le tampon en appelant ob_flush(); ou autres fonctions.
Il n'est jamais bon de faire tourner PHP en bourrique. Tous ces scénarios ont été prévus, certes, mais ne sont pas recommandés et peuvent mener à des comportements indéfinis.

La fonction de callback a aussi moyen de détecter 2 situations :

  1. si une fonction comme ob_flush() ou ob_clean() a été invoquée (opération sur le tampon mais sans le fermer) ;
  2. si la fermeture du tampon a été invoquée ("end").

Elle reçoit la situation en cours en second paramètre. Ce paramètre est un masque entre PHP_OUTPUT_HANDLER_START (1), PHP_OUTPUT_HANDLER_CONT (2) et PHP_OUTPUT_HANDLER_END (3).
Cela peut être pratique pour prendre des décisions dans des cas précis.

La fonction de callback sait quand elle est appelée
Sélectionnez
<?php
$bufferOperations = 0;

function dvp_callback($bufferContent, $mode)
{
    global $bufferOperations;
    
	/*
      Si une opération autre que la fermeture du tampon a été effectuée,
      la compter. Il peut s'agir soit d'un ob_clean(), soit d'un ob_flush(),
      on ne peut le savoir précisément.
    */
    if ($mode & PHP_OUTPUT_HANDLER_CONT) {
            $bufferOperations++;
    }
    if ($mode & PHP_OUTPUT_HANDLER_END) {
        // Ici la fermeture du tampon a été commandée, ob_end_flush() ou ob_end_clean();
    }
    return false; // Retourne le tampon comme il arrive
}

ob_start('dvp_callback');
echo "hello world";
ob_flush(); // "hello world" s'affiche
ob_end_clean();

echo $bufferOperations; // 1

Il est possible d'affecter une fonction de callback au tampon de sortie natif de PHP (chapitre I-B). Pour cela, indiquez son nom dans la directive output_handler de php.ini, en activant bien sûr le tampon de sortie PHP au moyen de output_buffering dans php.ini (une valeur autre que Off ou 0).

III. Les bonus

Ils sont documentés, mais bon...
output_add_rewrite_var() , je vais vous laisser lire la documentation, nul besoin de la répéter.
Cette fonction est intéressante car elle va elle-même démarrer un tampon en lui ajoutant une callback maison (nommée "URL-Rewriter") réécrivant les liens et les formulaires.

Concernant les fonctions de callbacks que PHP fournit (au moyen d'extensions, à activer), on peut citer :

  1. ob_gzhandler() : Gère la compression Gzip (conflit avec ob_inflate/deflatehandler()) ;
  2. mb_output_handler() : Gère des conversions de jeu de caractères en sortie ;
  3. ob_iconv_handler() : Gère des conversions de jeu de caractères en sortie ;
  4. ob_tidyhandler() : Gère l'analyse syntaxique du HTML de sortie pour réparations diverses ;
  5. ob_[inflate/deflate]handler() : Gère la compression Gzip (conflit avec ob_gzhandler()) ;
  6. ob_etaghandler() : Gère les en-têtes de cache HTTP "Etag" en sortie.

Certaines sont vraiment très efficaces, je pense à ob_iconv_handler() ou encore ob_tidyhandler(), essayez si vous ne connaissez pas.

Rappel: Si vous aviez codé des callbacks en PHP faisant le même travail que celles présentées dans la liste ci-dessus, utilisez plutôt celles de la liste: elles sont écrites en C et seront beaucoup plus rapides qu'une même implémentation écrite en PHP.

ob_list_handlers() retourne un tableau contenant les noms de callbacks en cours d'utilisation dans les éventuels différents tampons utilisateur ET éventuellement le tampon PHP.
Puis plutôt intéressante, ob_get_status() donne plein d'informations sur les tampons en cours d'utilisation.

Statistiques sur les tampons
Sélectionnez
<?php
ob_start();
echo "foo";
ob_start(function ($content) { return false; }, 200, false);
ob_start(null, 400);

$infos = ob_get_status(1); // Mettre le premier paramètre à true (ou 1)
for ($i = 0; $i < ob_get_level(); $i++) {
    ob_end_clean();
}

var_dump($infos);
/*
array(3) {
  [0]=>      *buffer 0, tout en bas*
  array(7) {
    ["chunk_size"]=>
    int(0)
    ["size"]=>
    int(40960) *40Ko*
    ["block_size"]=>
    int(10240)   * Allocations mémoires de 10Ko (segmentation) *
    ["type"]=>
    int(1)
    ["status"]=>
    int(0)
    ["name"]=>
    string(22) "default output handler"
    ["del"]=>
    bool(true)   * non verrouillé *
  }
  [1]=>
  array(5) {
    ["chunk_size"]=>
    int(200)
    ["type"]=>
    int(1)
    ["status"]=>
    int(0)
    ["name"]=>
    string(17) "Closure::__invoke"
    ["del"]=>
    bool(false)   * verrouillé *
  }
  [2]=>
  array(5) {
    ["chunk_size"]=>
    int(400)
    ["type"]=>
    int(1)
    ["status"]=>
    int(0)
    ["name"]=>
    string(22) "default output handler"
    ["del"]=>
    bool(true)
  }
}
*/

IV. Cas pratiques

A part les classiques (capturer la sortie de var_dump() ou de toute autre fonction), je n'en ai pas personnellement, mais on en trouve sur le web. Par exemple dans des scripts en ligne de commande écrits en PHP.
Imaginez un programme PHP qui doit télécharger un fichier en ligne, puis le copier sur le disque. Il pourrait être intéressant d'afficher une barre de progression du téléchargement (comme wget par exemple). Afficher peu à peu "=>" "==>" "===>" etc. pourrait être effectué avec un contrôle fin d'un tampon de sortie.
On peut extrapoler au web pour les mêmes cas, ou encore pour commencer à afficher du contenu à un internaute impatient, alors que l'on calcule encore un résultat (en général on fera plutôt l'inverse, c'est à dire allouer les ressources lourdes -comme une base de données- en début de script, pour finalement tout afficher d'un coup).

Coté Zend Framework, les tampons sont utilisés dans plusieurs cas pratiques. Regardez ainsi du coté de Zend_Controller_Response_Http, Zend_Controller_Dispatcher_Standard, Zend_Soap_Server, Zend_View_Helper_PlaceHolder et sa fameuse méthode captureStart(), etc.

V. Conclusions

Maîtriser la manière dont PHP "s'occupe de ce qu'il lui est demandé d'afficher" permet de comprendre un aspect du langage et certains comportements qu'il peut avoir que l'on juge souvent "bizarres".
Les tampons de sortie sont présents dans presque toutes les couches logicielles, afin d'éviter trop de "petites" communications trop souvent, souvent couteuses en performances notamment processeur. Lorsqu'il s'agit du réseau, TCP/IP fait un travail remarquable à ce sujet, et certains matériels réseaux ne se privent pas de mettre en tampon tout un tas de paquets avant de les expédier sur une autre interface pour éviter de surcharger le réseau.
Je m'écarte un peu non ?

J'espère que cet article aura fait germer des idées dans certaines têtes, ou au moins éclairci certains points sur une fonctionnalité de PHP souvent peu connue.

SAPI et modes de communication en PHP
PHP Internals : fonctionnement global de PHP

Remerciements à Mahefasoa pour les relectures et corrections.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2011 . Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.