Exemple XML XSLT avec PHP5
Transcription
Exemple XML XSLT avec PHP5
Exemple XML XSLT avec PHP5 Rédacteur: Alain Messin CNRS UMS 2202 Admin06 06/09/2007 Le but de ce document est de donner les principes de manipulation de données XML, via XSLT à l'aide de PHP5. Il ne s'agit ni d'un exposé théorique, ni d'une application complète, juste la mise noir sur blanc de quelques pistes concrètes pour aider à démarrer dans ces technologies pour ceux qui, comme moi, ne les connaissaient pas et qui ont du mal à débuter la dedans, après avoir lu quelques articles trop généralistes ou au contraire quelques livres trop compliqués. Je n'essaierai pas d'expliquer les formats XML ou XSLT, ce qui serait beaucoup trop long et dont je serais d'ailleurs bien incapable, mais je donnerais quelques références bibliographique et liens, et tenterais surtout d'expliquer les « non-dits » qui bloquent et font perdre de longues heures lorsqu'on débute dans une technologie. L'exemple qui est développé ici est un script qui permet, lors d'un appel AJAX (que je n'aborderais pas) de retourner le fragment XML nécessaire à la génération d'un menu déroulant (select), avec les choix correspondant aux débuts des mots saisis. Concrètement, ce script est appelé avec un paramètre debut égal aux caractères saisis par l'utilisateur, et on veut renvoyer la liste des mots (ici des pays) commençant par cette/ces lettre(s) comme réponse à l'interrogation. Plutôt que de le faire de manière classique (interrogation d'une base de donnée, puis renvoi d'une liste de mots séparés par des virgules par exemple), on va piocher les mots possibles dans un fichier XML, et renvoyer ce XML après filtrage et transformation par XSLT. On aura donc dans cet exemple 3 éléments à considérer: ● le fichier XML des données (ici on prendra donc une liste de pays) ● le script PHP appelé par la procédure AJAX ● le fichier XSLT permettant le filtrage et la transformation du XML. Le fichier XML: Le fichier XML qui est utilisé est généré par un script php qui interroge la base de données contenant la table des pays, et qui construit le fichier texte XML de manière tout à fait classique, le résultat d'une ligne de requête étant dans $row et une ressource de fichier dans $out: fputs ($out,utf8_encode("<?xml version='1.0' encoding='UTF-8' ?>\n")); fputs ($out,utf8_encode("<listePays>\n")); fputs ($out,utf8_encode("<!-- $date -->\n")); while ($row=mysql_fetch_assoc($res)) { fputs($out,utf8_encode(" <pays>\n")); fputs($out,utf8_encode(" <code_pays>".$row['code_pays']."</code_pays>\n")); fputs($out,utf8_encode(" <libelle_pays>".$row['libelle_pays']."</libelle_pays>\n")); fputs($out,utf8_encode(" </pays>\n")); } fputs ($out,utf8_encode("</listePays>\n")); Exemple XML XSLT avec PHP5 1/10 Le fichier donnees.xml ainsi créé est de la forme ci-dessous (vu avec un éditeur de texte normal style notepad): <?xml version='1.0' encoding='UTF-8' ?> <listePays> <!-- 4/09/2007, 12:08:28 --> <pays> <code_pays>AF</code_pays> <libelle_pays>AFGHANISTAN</libelle_pays> </pays> <pays> <code_pays>ZA</code_pays> <libelle_pays>AFRIQUE DU SUD</libelle_pays> </pays> <pays> <code_pays>AL</code_pays> <libelle_pays>ALBANIE</libelle_pays> </pays> ... <pays> <code_pays>ZM</code_pays> <libelle_pays>ZAMBIE</libelle_pays> </pays> <pays> <code_pays>ZW</code_pays> <libelle_pays>ZIMBABWE</libelle_pays> </pays> </listePays> On trouve donc en première ligne (c'est impératif que ce soit en première ligne, le < devant être le premier caractère du fichier) la déclaration du format XML, la version est toujours 1.0, on précise que le codage des caractères est en UTF-8 (on a vu que l'écriture du fichier XML a été faite en UTF-8. Cette première ligne n'est pas obligatoire, mais permet d'éviter les ambiguïtés. On peut également préciser dans cette déclaration standalone='yes', qui indique que le XML en question est auto-suffisant et ne requiert pas de lien avec une DTD (Document Type Definitions). Un document XML doit contenir une et une seule racine, ici indiquée par la balise listePays.(Toutes les balises XML doivent être fermées et ne pas être entrelacées, et sont sensibles à la casse). Suit une ligne de commentaire indiquant la date de génération de la liste, puis les différents pays, encadrés par la balise « pays » et décrits aux moyens des balises « code_pays » et « libelle_pays ». Notez que l'indentation du fichier telle que générée par le script de création est tout à fait optionnelle. On peut supprimer les sauts de lignes et les blancs dans le script de génération, on obtiendra alors un fichier « à plat », moins lisible mais plus court. Il est même préférable d'utiliser cette version « à plat », car les traitements XML sont transparents aux blancs et sauts de lignes, qui apparaissent dans le document XML de sortie en l'alourdissant inutilement. On verra toutefois comment alléger le document de sortie en utilisant la transformation XSLT. Notez aussi, et c'est en fait ce qui fait la puissance du XML, que le format des données ainsi décrites peut être complété à l'envie, par exemple en ajoutant le nombre d'habitants d'un pays, le continent etc..., en ajoutant de nouvelles balises, sans que notre traitement soit le moins du monde modifié, on le précisera dans la conclusion. Exemple XML XSLT avec PHP5 2/10 Le fichier XSLT Je vais décrire ce fichier avant le script PHP, puisqu'il devra être appelé au même titre que le fichier de données pour permettre la génération du fichier XML de sortie. Autant le format XML des données présenté ci-dessus est facilement compréhensible, même sans connaissances de XML, autant le format XSLT est déjà plus ésotérique; déjà, XSL (Extensive Stylesheet Language se décline en XSLT (Transformations) qui sert à la transformation de l'XML et en XSL-FO destiné à formater les objets (Formatting Objects). Ici nous utiliserons donc XSLT, tout en remarquant que c'est en fait la même chose et toujours du XML. En effet, XSLT est d'abord un fichier XML comportant des balises spécifiques, qui sont interprétées par un moteur qui peut être à bord d'un navigateur, une application autonome, ou dans notre cas, implanté dans une fonction de bibliothèque de langage. Pour mieux décrire ce fichier, je vais en fait en faire deux exemples; le premier réalisera (une fois « exécuté » dans le cadre de notre script PHP) une simple mise en forme du fichier XML d'entrée, pour le convertir en une page web affichant le tableau des pays. Puis je décrirais le fichier XSLT qui réalisera vraiment ce que l'on veut, à savoir un filtrage et une conversion XML. Conversion en page web: C'est l'exemple que l'on donne le plus souvent, on verra qu'il est trompeur. Soit donc le fichier texte suivant, transform.xsl <?xml version="1.0" encoding='UTF-8' ?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:template match="/"> <html> <title>Liste des codes des pays</title> <body> <h1>Liste des codes des pays</h1> <table> <tr><th>Nom du Pays</th><th>Code du Pays</th></tr> <xsl:apply-templates /> </table> </body> </html> </xsl:template> <xsl:template match="pays"> <tr> <td><xsl:value-of select="libelle_pays"/></td> <td><xsl:value-of select="code_pays"/></td> </tr> </xsl:template> </xsl:stylesheet> L'exécution de la transformation de notre fichier XML des pays donnees.xml au moyen de transform.xsl donnera une page web simple affichant la table des pays comme suit: <html> <title>Liste des codes des pays</title> <body> <h1>Liste des codes des pays</h1> <table> <th>Nom du Pays</th> <th>Code du Pays</th> Exemple XML XSLT avec PHP5 3/10 </tr> <tr> <td>AFGHANISTAN</td> <td>AF</td> </tr>... <tr> <td>ZIMBABWE</td> <td>ZW</td> </tr> </table> </body> </html> On obtient donc une page html correcte, bien que pas très bien indentée, qui s'affiche effectivement correctement comme une table des pays. Explications du format: On retrouve en première ligne la déclaration XML, puis une balise indiquant que ce XML est une « stylesheet » (c'est une balise, ce qui veut dire qu'il faudra la fermer à la fin du fichier, c'est en fait la racine de notre document XML/XSLT), mais il est obligatoire de déclarer un espace de nom (xml name space), au contraire de XML ou l'on peut s'en passer (je n'insisterai pas là dessus, il suffit de faire figurer cette ligne, sauf dans le cas où l'on doit gérer plusieurs espaces, mais ce ne sera pas le cas ici). L'espace de nom est donc xsl et devra préfixer toutes les balises de notre fichier XSLT. La ligne suivante exprime un modèle (template) à exécuter si la condition « match » est réalisée; ici match="/" exprime que le modèle sera appliquée à la racine et donc une seule fois, puisque la racine est unique. Les balises HTML seront recopiées telle que, mais on verra que cela ne fonctionne que (presque) par hasard, et qu'il n'est pas forcément sain d'utiliser ce mode de transposition pour des balises HTML, on verra comment le faire plus correctement (à mon avis!). Ce modèle appliqué à la racine du document XML va donc fournir le corps de notre document HTML de sortie (entête, déclaration du corps et de la table), mais il faut aussi et surtout être capable de générer les lignes de la table. Pour cela, on a fait appel à une ligne apply-templates, qui va exécuter les modèles définis dans notre XSLT (autres que le modèle déjà appliqué à la racine). Ici, il n'y aura qu'un autre modèle, défini en fin de fichier , et qui est très simple. Il s'applique à toutes les balises de nom « pays », et outre l'encadrement par <tr> et </tr> des lignes, positionne les valeurs pour chaque ligne. Les balises « value-of » permettent de positionner les valeurs à envoyer, on voit ici que le select permet de renvoyer les valeurs du code_pays et du libelle_pays correspondant aux balises de mêmes noms. Conversion et filtrage XML Mon fichier transform.xsl sera donc modifié pour formater différemment la sortie après traitement, car je veux en fait transmettre mes données sous une forme permettant simplement la génération d'un select; je veux donc disposer d'un fichier XML de la forme: <resultats> <option value="code_pays">libelle_pays<option> ... <resultats> Exemple XML XSLT avec PHP5 4/10 De plus, je veux n'envoyer que les pays dont le libellé commence par le contenu d'un paramètre $debut. Je vais commencer par modifier transform.xsl pour faire une conversion XML->XML. On pourrait partir du fichier suivant, qui devrait mettre tous les pays dans le bon format: <?xml version="1.0" encoding='UTF-8' ?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:template match="/"> <resultats> <xsl:apply-templates /> </resultats> </xsl:template> <xsl:template match="pays"> <option value="<xsl:value-of select="code_pays />"> <xsl:value-of select="libelle_pays" /> </option> </xsl:template> </xsl:stylesheet> Explications du format: Le principe est le même que pour la transformation en HTML: on génère les balises pour la racine « resultats », puis pour chaque pays, on va appliquer le modèle qui va générer « option ». Ce fichier ne fonctionne pas, et je me suis cassé la tête en me demandant pourquoi et comment gérer du texte et notamment les < sans les convertir en <, ou en les convertissant et dans ce cas ils apparaissent tel que dans le fichier de sortie... jusqu'à ce que je comprenne mon erreur fondamentale. En réalité, dans cet exemple, on parle de balises et on se calque dans le raisonnement sur le HTML et le texte contenu dans les fichiers, alors que conceptuellement, il faut en fait raisonner sur le DOM (Document Object Model) et considérer que nos fichiers textes vont permettre de créer des objets DOMs à partir de leurs indications, et que ce sont ces DOMs qu'il faut gérer, et non des balises ou du texte. Il faut donc créer des noeuds et des éléments de DOM. C'est d'ailleurs finalement beaucoup plus clair et plus simple. Au lieu de faire faire figurer mes balises en tant que texte dans mon fichier, je vais les fabriquer en tant qu' objets DOM. Ainsi, la confusion entre balises, textes et objets ne se fera plus, et j'aurai quelque chose de conceptuellement correct. Je reprend donc mon fichier et je crée des éléments du DOM au lieu de créer du texte! <?xml version="1.0" encoding='UTF-8' ?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:template match="/"> <xsl:element name="resultats"> <xsl:apply-templates /> </xsl:element> </xsl:template> <xsl:template match="pays"> <xsl:element name="option"> <xsl:attribute name="value"> <xsl:value-of select="code_pays" /> </xsl:attribute> <xsl:value-of select="libelle_pays" /> </xsl:element> </xsl:template> Je crée donc l'élément « resultats », puis j'appelle mon modèle pour les pays, dans lequel je crée l'élément « option », que j'affecte naturellement de l'attribut « value », qui aura donc pour Exemple XML XSLT avec PHP5 5/10 valeur le code_pays, la valeur de l'élément étant libelle_pays. Le fichier ainsi modifié est propre et respectueux du DOM. Mais il ne fait pas encore ce que veux, c'est à dire ne sélectionner que les pays dont le libellé commence par $debut. Je vais donc modifier le modèle « pays » pour introduire une condition dépendant d'un paramètre, et introduire ce paramètre: <?xml version="1.0" encoding='UTF-8' ?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:param name = "debut" ></xsl:param> <xsl:template match="/"> <xsl:element name="resultats"> <xsl:apply-templates /> </xsl:element> </xsl:template> <xsl:template match="pays"> <xsl:if test="substring(libelle_pays,1,string-length($debut))=$debut" > <xsl:element name="option"> <xsl:attribute name="value"> <xsl:value-of select="code_pays" /> </xsl:attribute> <xsl:value-of select="libelle_pays" /> </xsl:element> </xsl:if> </xsl:template> <xsl:template match="text()|@*"> <xsl:value-of select="''"/> </xsl:template> </xsl:stylesheet> J'ai donc introduit le paramètre « debut » après la déclaration « stylesheet », par l'instruction « param », de façon à ce que ce paramètre puisse être reconnu dans tous les modèles (portée globale, car défini au niveau le plus haut). Par contre, je n'ai pas indiqué de valeur, il faudrait mettre G pour obtenir tous les pays dont le libelle commence par G etc.. Dans le modèle « pays », j'ai ajouté un test conditionnel (« if »). Si la condition est remplie, les instructions englobées sont exécutées, sinon non. La condition est bien sur: début du libelle du pays = $debut. A noter qu'il existe peu de fonction intégrées dans XSLT, et que c'est une difficulté importante; par exemple il n'existe pas de conversion de casse, il faut éventuellement utiliser la fonction translate et des listes de caractères, ce qui rend la fonction dépendante de la langue. Ici, j'ai ignoré la question, tous les libellés sont en majuscules, il suffira que $debut le soit aussi. Bien sur, un paramètre fixe n'est pas d'une grande utilité, je montrerai dans l'explication qui suit, consacrée au script PHP, comment résoudre la question. Encore un dernier point par rapport aux filtrage de textes incorporés dans les fichiers XML, liés par exemple aux espaces et saut de ligne d'indentation. Si l'on examine le source du fichier XML renvoyé suite au traitement effectué par la transformation XSLT, on s'aperçoit de la présence de nombreuses lignes vides au milieu des champs renvoyés des pays résultants. Ces lignes correspondent aux indentations des balises du fichier source, qui n'ont pas été filtrées. Pourquoi? Parce qu'une règle par défaut est exécutée lors des transformations XSLT, qui recopie dans le fichier résultant les noeuds textes du XML traité. Donc toutes les données filtrées ont bien été supprimées du résultats, mais les sauts de lignes et espaces associés, existant dans le DOM comme noeuds textes, ont été gardés. Pour éviter cette inconvénient (qui augmente inutilement le volume transféré vers le client), on peut utiliser un fichier XML de départ sans espaces ni sauts de lignes, ou simplement ajouter un modèle de plus à notre XSLT, qui va ignorer les noeuds textes (et qui générera du coup un fichier plus compact). Le modèle exécuté par défaut est : Exemple XML XSLT avec PHP5 6/10 <xsl:template match="text()|@*"> <xsl:value-of select="."/> </xsl:template> Ce qui recopie (value-of select=".") tous les noeuds textes et attributs sur la sortie. Il suffit de remplacer le . (qui veut dire « contenu de l'élément courant ») par "''" (2 apostrophes simples entourés des deux guillemets, ce qui indique une chaîne vide), pour être débarrassé de ces gêneurs. C'est ainsi que le dernier modèle du fichier permet ce filtrage. Ce modèle est appliqué comme le précédent, suite à l'instruction « apply-templates » du modèle principal. Le script PHP Je me place dans la prospective, en utilisant uniquement PHP5, considérant que PHP4 est bientôt dépassé et que seul PHP5 possède des fonctionnalités intéressantes en terme de XML, de XSLT et de DOM. L'aide sur ces fonctions sera obtenu facilement sur le site php.net, en recherchant les fonctions DOM (et non pas DOM XML valables en PHP4) et XSL (et non pas XSLT valable en PHP4). Le script PHP, qui sera donc appelé par ajax en passant le paramètre début par GET, va effectuer 6 fonctions, en fait très sobres: ● prise en compte du paramètre début ● lecture des données XML ● lecture du fichier XSLT ● introduction du paramètre début dans le traitement ● transformation des données ● envoi au client On voit que le script est très simple: outre les déclarations des noms de fichiers XSLT et XML, puis l'acquisition du paramètre $debut, il suffit de créer un document DOM à partir des données XML en chargeant le fichier XML en mémoire grâce à la fonction DomDocument::load, Ensuite, on crée le processeur XSLT (nouvelle instance de la classe xltProcessor), dans lequel on importe le fichier XSLT(import(StyleSheet) après l'avoir créer en tant qu' objet DOM (toujours avec la fonction DomDocument::load puisque XSLT est avant tout un document XML!). L'instruction setParameter sur l'objet $xslt permet de fixer le paramètre $debut dans le DOM XSLT en mémoire. Enfin, on réalise la transformation (transformToXML) et on l' envoie (echo). $fichierXSLT="transform.xsl"; $fichierXML="donnees.xml"; // prise en compte du paramètre debut if (isset($_GET['debut'])) { $debut = utf8_decode($_GET['debut']); } else { $debut = ""; } // conversion en majuscules car tous les noms sont en majuscules // et XSLT pas fort pour gerer cela $debut = strtoupper($debut); /// ! attention utilisation des fonctions DOM Exemple XML XSLT avec PHP5 7/10 // creation du document DOM a partir des donnees XML $xml=DomDocument::load($fichierXML); // creation du processeur XSLT $xslt = new xsltProcessor; $xslt->importStyleSheet(DomDocument::load($fichierXSLT)); // introduction du paramètre $debut dans le DOM du processe $xslt->setParameter('','debut',$debut); // traitement et envoi des données echo $xslt->transformToXML($xml); Précisions sur le type de fichier envoyé: Il est possible, pour lever tout doute au client sur la nature du fichier envoyé, de faire précéder l'envoi des données par une instruction: header('Content-Type: text/xml;charset=utf-8'); Ceci indiquera au client que le fichier reçu est du XML. De la même façon, on a vu qu'aucune entête XML n'était paramétrée dans le fichier XSLT. Pourtant en examinant le fichier source reçu par le client, on constate qu'une entête XML à été insérée: <?xml version="1.0"?> En fait le processeur suppose que le résultat de la transformation est du XML dès que l'entête n'est pas la balise « HTML », auquel cas il ajoutera une entête HTML au résultat. On peut forcer et paramétrer cette dernière en insérant juste après la ligne incluant la balise « stylesheet » l'instruction: <xsl:output method="xml" /> Conclusion L'utilisation d'un fichier de données XML et d'une transformation XSLT ont permis de réaliser l'opération voulue, ce qui aurait put être fait avec les techniques classiques, mais les techniques utilisées ici ont un certain nombre d'avantages: ● robustesse vis à vis des formats de données; en entrée comme en sortie, des modifications peuvent être faites sur les formats de données sans impacter le fonctionnement du traitement décrit. Considérons le format XML suivant, qui décrit non plus les pays, mais les personnes, en incluant les noms, prénoms, adresses, codes postaux, et autres caractéristiques de ses personnes, en plus des codes et des libellés de leurs pays. Ce fichier de données XML pourrait être par exemple au format suivant: <?xml version='1.0' encoding='UTF-8' ?> <listePersonnes> <!-- 4/09/2007, 12:08:28 --> <personne> <identite> <nom>Toto</nom> Exemple XML XSLT avec PHP5 8/10 <prenom>Dudule</prenom> </identite> <adresse> <rue>Rue Droite</rue> <ville>Grasse</ville> <code_postal>06130</code_postal> <pays> <code_pays>FR</code_pays> <libelle_pays>FRANCE</libelle_pays> </pays> </adresse> <hobby>Peinture</hobby> </personne> <personne> <identite> <nom>Schmidt</nom> <prenom>Hans</prenom> </identite> <adresse> <rue>Wurst Strasse</rue> <ville>Hambourg</ville> <code_postal>22767</code_postal> <pays> <code_pays>DE</code_pays> <libelle_pays>ALLEMAGNE</libelle_pays> </pays> </adresse> <hobby>Cuisine</hobby> </personne> ... </listePersonnes> On peut faire le test, le traitement précédent s'applique sans aucun changement à ce nouveau fichier: pourquoi? ○ le fichier de données est un document XML, le script le chargera donc comme l'ancien et en créera un objet DOM en mémoire. ○ les modèles contenus dans le fichier XSLT s'appliquent: ■ à la racine (/) et donc à "listePersonnes" comme à "listePays" ■ aux balises "pays", "code_pays" et "libelle_pays" qui existent toujours avec les mêmes significations. ● abstraction du script: le script php est utilisable quels que soient les données à traiter et le type de traitement effectué. La seule particularité est l'existence d'un paramètre début. Mais on peut très bien utiliser ce même script avec les mêmes données en entrée, mais avec un traitement différent (par exemple retourner les personnes dont le nom commence par $debut et non le libellé du pays), ou effectuer un traitement sur un fichier de données complètement différent etc... ● abstraction du format de sortie: on peut imaginer que le traitement envoie différentes données, en plus des données « option ». Le programme qui reçoit le fichier XML traitant les balises « option », n'en sera aucunement perturbé. Cela permettra, par exemple d'effectuer un traitement sur plus de paramètres dans certains cas, le programme « ancien » ne gérant que les balises « option » n'étant pas à modifier. Exemple XML XSLT avec PHP5 9/10 ● modification complète du traitement: on a vu qu'un traitement différent permettait d'afficher une page web, on peut (et ça se fait!) générer d'autres formats d'affichage (wap pour le téléphones par exemple, ou même synthétiser la voix). Liens Introduction aux techniques web http://www.w3schools.com/ XSLT: http://www.zvon.org/xxl/XSLTreference/Output/index.html http://www.cafeconleche.org/books/xmljava/chapters/ch17.html#d0e31297 Recommandation XML du W3C: http://www.w3.org/TR/2006/REC-xml-20060816/#NT-XMLDecl Recommandation XSLT du W3C: http://www.w3.org/TR/xslt PHP: http://www.php.net Livres Eyrolles Bien développer pour le Web 2.0 Christophe Porteneuve Wrox Beginning XML David Hunter et al. Wyley XML for Dummies Exemple XML XSLT avec PHP5 10/10