Avec l'arrivée de PHP 5.2 il est maintenant possible d'implémenter une barre de progression pour observer l'avancement d'un upload de fichier sans faire appel à des langages tiers ou la mise en place d'une extension exotique.
Une démo est disponible ici, mais en regardant les sources la logique de mise en place est noyé dans les bibliothèques javascript et les infos sur le sujet se font (pour l'instant) plutôt rares.
Le but de ce tutoriel est de montrer la base de cette mise en place et d'avoir une idée de son fonctionnement.
C'est pourquoi le code est volontairement dépouillé et ne devrait pas servir en production.
En d'autres termes il n'y a aucune gestion d'erreur/sécurité et encore moins d'optimisations...sans compter que le code js ne sera de toute manière compatible qu'avec firefox ^^
Il sera donc vu l'environnement nécessaire au bon déroulement des opérations, la mise en place du code html, les informations disponibles pour évaluer la progression en php, le code javascript pour interroger le serveur et ramener les informations liées à cette progression. Pour finir, un exemple de code fonctionnel pour tester l'outil à l'aide d'un copier-coller (mon hébergeur n'est toujours pas à la page).
Par effet de bord seront également abordés des sujets comme les extensions pecl, xmlHttpRequest, l'échange client-serveur en json ou encore l'upload d'un fichier en mode asynchrone grace à une iframe cachée.
Un blog qui parle de cette fonctionnalité (en)
Apc est une extension pecl servant à implémenter un système de cache d'opcode et sera disponible en natif avec la version 6 de php.
Ce module est le seul disponile pour permettre d'obtenir les informations de progression.
Sous linux il est pour l'instant (décembre 2006) obligatoire de compiler l'extension à partir des sources du cvs pour avoir accès aux fonctionnalités de suivie d'upload.
Voir ce blog pour les sources adéquats et une manière d'installer l'extension sous windows.
Pour ma part j'ai testé l'ensemble sous kubuntu dapper avec PHP 5.2 (compilé) et apache 1.3.
Une fois passé ces formalités, il reste deux directives à mettre en place dans le php.ini :
;c'est une nouvelle directive de configuration pour apc, elle doit être à on pour le suivie de l'upload apc.rfc1867 = on ;chargement du module apc extension="apc.so" ;chemin vers le répertoire des modules extension_dir= "/chemin/vers/bon/repertoire/"
Plus d'infos sur APC
Installer une extension PECL
Les sources APC du cvs Merci à Mike qui les a posté sur le blog cité précédemment
APC pour windows (non testé)
La RFC 1867
Le code html est similaire à un upload classique, il faut toutefois rajouter un champs hidden à l'intérieur du formulaire contenant l'input file :
<input type="hidden" name="APC_UPLOAD_PROGRESS" id="id_progress" value="12345"/>
Ce champs sert à stocker une clef qui servira à reconnaitre le formulaire à observer côté serveur (plusieurs input file peuvent être placés dans un même formulaire, dans ce cas la progression se basera sur le poids total de l'ensemble des fichiers)
L'attribut name, APC_UPLOAD_PROGRESS, sert au fonctionnement interne de php (insensible à la casse).
Voici comment récupérer les informations nécessaires en PHP :
$tabInfosProgress=apc_fetch('upload_'.$idProgress);
$idProgress correspond à la valeur du champs hidden vu précédemment.
Le préfixe "upload_" précédant $idProgress correspond à un besoin interne de php pour qu'il puisse retrouver ces petits.
$tabInfosProgress recevra les informations liées à la progression sous la forme d'un tableau associatif :
Une fois l'upload achevé de nouvelles informations s'ajoutent au tableau :
En bonus l'ensemble des valeurs de retour possibles de cancel_upload :
A noter que même si l'upload est annulé la valeur de "done" sera à 1.
Le but étant d'informer l'internaute sur l'état de la progression, l'objet xmlHttpRequest interrogera régulièrement le serveur pour retourner les informations en temps réel.
Il suffit de récupérer la clef d'upload du fichier (valeur de APC_UPLOAD_PROGRESS) et de l'envoyer à un script php pour obtenir les informations.
function chercheInfos() {
//la clef qui identifie le formulaire à observer
var idProgress=document.getElementById("id_progress").value;
var xhr=new XMLHttpRequest();
//la requête est envoyé en mode asynchrone(paramètre true) pour éviter de geler le navigateur
xhr.onload=tcb; //la fonction de rappel qui recevra les informations
xhr.open("GET","progress.xhr.php?id_progress="+idProgress,true);
xhr.send(null);
}
Les données sont envoyées en GET pour éliminer des lignes de code, mais la méthode POST marche aussi très bien.
Plus d'infos sur xmlHttpRequest
Le but est d'envoyer le fichier en mode asynchrone pour permettre à la page de ne pas se recharger une fois l'upload terminé.
Cela servira à visualiser la réponse final du serveur.
<form enctype="multipart/form-data" target="tfrm" action="index.php" method="POST" onsubmit="testProgress();"> <input type="hidden" name="APC_UPLOAD_PROGRESS" id="progress_key" value="<?php echo uniqid()?>"/> <input type="file" id="test_file" name="test_file"/><br/> <input type="submit" value="Upload!"/> </form> <div id="rep" style="width:400px; height:200px; margin:10px; padding:10px;"> <span id="enCours"></span> Ko sur <span id="total"></span> Ko <p>Tableau des infos de progression au format json :</p> <p id="tab"></p> </div> <iframe id="frm" name="tfrm" style="display:none;"></iframe
Le mode asynchrone est obtenu grace à l'utilisation d'une iframe caché (display:none) qui est relié au formulaire à l'aide de la propriété target de la balise form qui prend pour valeur la propriété name de l'iframe.
De cette façon la soumission du formulaire se fera à partir de la "page" de l'iframe et non pas à partir de la page principal.
L'input hidden reçoit comme valeur un indentifiant unique généré en php.
Cette identifiant n'aura pas besoin d'être regénéré en cas d'uploads successifs à partir de la page principal.
La fonction javascript testProgress() relié à l'évènement onsubmit s'occupera d'amorcer les requêtes vers le serveur pour obtenir les informations de progression.
La balise div (id="rep") s'ocupera d'afficher les informations de progression via une gestion en javascript.
if (isset($_GET['progress_key'])) {
//$rep sera égal à false si la clef n'existe pas dans le cache apc
$rep=apc_fetch('upload_'.$_GET['progress_key']);
echo json_encode($rep);
exit;
}
Ce script sera interrogé via javasript à l'aide de l'objet xmlHttpRequest. Le format de la réponse sera envoyé en json.
json est un format utilisé par javascript pour la création de variables de type tableau ou objet (les objets sont des tableaux en js).
Un exemple de notation json :
var exempleJson={
toto:valeurToto,
titi:valeurTiti
};
var valToto=exempleJson.toto;
Depuis PHP 5.2 le module json est intégré en natif à php (anciennement via pecl).
json_encode() permet de sérialiser un tableau php en notation json compréhensible par javascript.
Plus d'infos sur json
Plus d'infos sur le module php json
Le module json et l'ISO-8859-1
function testProgress() {
var idProgress=document.getElementById("progress_key").value;
var xhr=new XMLHttpRequest();
xhr.onload=tcb; //la fonction de rappel qui gère la réponse du serveur
xhr.open("GET","progress.xhr.php?progress_key="+idProgress,true);
xhr.send(null);
}
function tcb() {
var repXhr=this.responseText; //récupération de la réponse du serveur via l'objet xmlHttpRequest (this)
/*
La réponse envoyé par le serveur étant au format texte il faut utiliser eval() pour la manipuler
La réponse au format json ne peut être exploité directement par eval,
il faut l'entourer de parenthèses via une concaténation pour éviter un bug
*/
var objRep=eval("("+repXhr+")");
//affichage des informations
document.getElementById("enCours").innerHTML=objRep.current;
document.getElementById("total").innerHTML=objRep.total;
document.getElementById("tab").innerHTML=repXhr;
//tant que l'upload est en cours le serveur est réinterrogé
if (objRep.done==0) { testProgress(); }
}
<?php
header('Content-type: text/plain; charset=UTF-8');
if (isset($_GET['progress_key'])) {
$rep=apc_fetch('upload_'.$_GET['progress_key']);
echo json_encode($rep);
exit;
}
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>== Progress ==</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <script type="text/javascript" src="index.js"> </script> </head> <body> <form enctype="multipart/form-data" target="tfrm" action="index.php" method="post" onsubmit="attendEnvoie();"> <input type="hidden" name="APC_UPLOAD_PROGRESS" id="progress_key" value="<?php echo uniqid()?>"> <input type="file" id="test_file" name="test_file"><br> <!-- histoire de tester la réaction lors d'un envoie multiple <input type="file" id="test_file2" name="test_file2"><br> --> <input type="submit" value="Upload!"> <input type="button" onclick="annule();" value="Annule"> </form> <div id="rep" style="width:400px; height:200px; margin:10px; padding:10px;"> <span id="enCours"></span> Ko sur <span id="total"></span> Ko <p>Tableau des infos de progression au format json :</p> <p id="tab"></p> </div> <iframe id="tfrm" name="tfrm" style="display:none;"></iframe> </body> </html>
Quelques fonctionnalités en plus :
- Ajout de la fonction attentEnvoie() pour éviter un bug (la requête xhr doit partir après la requête du navigateur)
- Ajout d'un bouton d'annulation pour stopper l'upload (fonction annule())
/*
Temporise avant l'envoie de la requête pour éviter un bug
*/
function attendEnvoie() {
setTimeout(testProgress,50);
}
/*
Construit la requête xmlHttpRequest
*/
function testProgress() {
var idProgress=document.getElementById("progress_key").value;
var xhr=new XMLHttpRequest();
xhr.onload=tcb; ////la fonction de rappel qui gère la réponse du serveur
//la requête est envoyé en mode asynchrone(paramètre true) pour éviter de geler le navigateur
xhr.open("GET","progress.xhr.php?progress_key="+idProgress,true);
xhr.send(null);
}
/*
Permet d'annuler l'envoie des infos par le navigateur via la méthode stop() de l'objet window
Peut provoquer le plantage du navigateur(ff2), à affiner ^^
*/
function annule() {
//récupération de l'objet window de l'iframe
var winIfrm=document.getElementById("tfrm").contentWindow;
winIfrm.stop();
}
/*
La fonction de rappel de l'objet xmlHttpRequest
*/
function tcb() {
var repXhr=this.responseText; //récupération de la réponse du serveur via l'objet xmlHttpRequest (this)
/*
La réponse envoyé par le serveur étant au format texte il faut utiliser eval() pour la manipuler
La réponse au format json ne peut être exploité directement par eval,
il faut l'entourer de parenthèses via une concaténation pour éviter un bug
*/
var objRep=eval("("+repXhr+")");
document.getElementById("enCours").innerHTML=objRep.current;
document.getElementById("total").innerHTML=objRep.total;
document.getElementById("tab").innerHTML=repXhr;
//tant que l'upload est en cours le serveur est réinterrogé
if (objRep.done==0) { testProgress(); }
}
Erreurs, suggestions, commentaires, ... -> thierry at fassnet dot net
Thierry Sottani - Création de site Internet à Nice - Développeur Php/Flex Indépendant