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 &lt;, 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