Manipuler du XML de manière élégante avec Symfony2
Cet article à pur objectif de vous présenter un bundle Symfony2 que j’ai développé. “NotJaxbBundle” Ce bundle permet de manipuler facilement du XML plus ou moins complexe pour communiquer avec des API, sauvegarder des données structurées etc… En route !
L’origine
A l’origine de ce bundle, le besoin de communiquer facilement avec un moteur de recherche “Antidot”. Pour ceux qui connaissent, Antidot est un moteur de recherche vraiment bien foutu et très complet (ne cherchez pas c’est payant). Le problème c’est que pour être complet les informations retournées par Antidot (au format XML) sont complexes. En PHP, on croise souvent des bouts de code qui utilisent des requêtes XPath ou du SimpleXML pour lire du XML, mais dans le cas de XML complexe c’est fastidieux et très peu maintenable. Faisant un peu de Java, j’ai donc cherché un équivalent aux API SAX etc… sans succès. En fait, un bundle existe (JMSSerialiser) mais pas assez complet pour mon besoin (namespace, récursivité etc…). J’ai donc essayé de développer un bundle perso. Son nom aurait put être PaxbBundle mais bon, NotJaxb c’était plus drôle…
Comment ça fonctionne ?
Le système repose sur les annotations que l’on place dans les commentaires PHP des objets. Chaque annotation détermine comment lire ou écrire l’attribut de la classe (ou la classe elle même). Il existe donc plusieurs annotations :
- @XmlObject : Décrit un objet qui deviendra un nœud XML.
- @XmlAttribute : Décrit un attribut XML.
- @XmlValue : Décrit une valeur d’un nœud XML.
- @XmlElement : Décrit un objet qui deviendra une structure XML.
- @XmlList : Décrit un tableau qui sera transformé en liste de nœud XML.
Mise en place étape par étape :
- Ecrire un objet à sérialiser ou désérialiser.
- Apposer les annotations sur la classe et les attributs pour décrire la structure.
- Utiliser le service de mashalling (PHP vers XML) ou unmarshalling (XML vers PHP).
Rien de compliquer donc… Enfin si, car rien ne fonctionne du premier coup. Si vous imbriquez plusieurs dizaine d’objets entre eux, c’est quand même un peu long à mettre en oeuvre. Mais c’est efficace, et surtout très facilement maintenable. Il n’y a pas de code de mapping à écrire, et vous bénficiez des objets PHP avec les mutators qui vont bien.
Exemple XML vers PHP et PHP vers XML
L’exemple complet est disponible ici : https://github.com/Level42/NotJaxbBundle/tree/master/Tests/Entity Je veux obtenir ou lire le XML suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?xml version="1.0" encoding="UTF-8"?> <personnes> <personne id="1"> <nom>Dupont</nom> <service>Personnel</service> <adresses> <adresse id="1"> <numero>12</numero> <rue>des docks</rue> <cp>69000</cp> <ville>LYON</ville> </adresse> <adresse id="2"> <numero>32</numero> <rue>place de la libération</rue> <cp>75000</cp> <ville>PARIS</ville> <complement>Complément 1</complement> <complement>Complément 2</complement> <complement>Complément 3</complement> </adresse> </adresses> </personne> </personnes> |
- Personnes : C’est l’objet qui va transiter.
- Personne : C’est l’objet qui constitue la liste contenues dans “Personnes“
- Adresse : C’est un l’objet qui représente les adresses associées à chaque “Personne”
- Complement (optionnel) : C’est un objet qui peut être contenu dans l’objet “Adresse”.
On va donc créer nos 4 objets PHP, avec les attributs et mutators associés. Ensuite, les annotations :
1 2 3 4 5 6 7 8 9 |
/** * @XmlObject(name="Personnes") */ <strong>class Personnes ...</strong> /** * @XmlList(name="personne", type="Level42NotJaxbBundleTestsEntityPersonne") */ private $personnes; ... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/** * @XmlObject(name="Personne") */ <strong>class Personne ...</strong> /** * @XmlAttribute */ private $id; /** * @XmlElement */ private $nom; /** * @XmlElement */ private $service; /** * @XmlList(name="adresse", wrapper="adresses", type="Level42NotJaxbBundleTestsEntityAdresse") */ private $adresses; ... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/** * @XmlObject(name="Adresse") */ <strong>class Adresse ...</strong> /** * @XmlAttribute */ private $id; /** * @XmlElement */ private $numero; /** * @XmlElement */ private $rue; /** * @XmlElement(name="cp") */ private $codePostal; /** * @XmlElement */ private $ville; /** * @XmlList(name="complement", type="Level42NotJaxbBundleTestsEntityComplement") */ private $complements; ... |
1 2 3 4 5 6 7 8 9 |
/** * @XmlObject(name="Complement") */ <strong>class Complement ...</strong> /** * @XmlValue */ private $valeur; ... |
Et maintenant, place à la magie 🙂 Passer de cet objet à du XML :
1 |
$xml = $this->service->marshall($personnes); |
Passer d’un XML à cet objet :
1 |
$personnes = $this->service->unmarshall($xml, 'Level42NotJaxbBundleTestsEntityPersonnes'); |
Utilisation hors de Symfony2
Il est également possible d’utiliser le bundle hors de Symfony2. Il a été avec succès mis en oeuvre dans des architectures Magento et Drupal, mais on peux l’utiliser “nude”. Pour cela, rien de plus simple : Un fichier composer.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "name": "level42/santoshkumar-example", "description": "An untested (sorry) example", "version": "0.1-dev", "require": { "php": ">=5.3.3", "doctrine/common": "2.3.*", "level42/notjaxb-bundle": "1.*" }, "require-dev": { "phpunit/phpunit": "3.7.*" }, "minimum-stability": "stable" } |
Un petit fichier autoload.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php require_once __DIR__ . '/vendor/autoload.php'; /** * Load classes from path * @param unknown $path */ function loadClasses($path) { if ($handle = opendir($path)) { while (false !== ($entry = readdir($handle))) { if (substr($entry, strlen($entry) - 4, strlen($entry)) == '.php') { require_once $path . '/' . $entry; } } closedir($handle); } } loadClasses(__DIR__ . '/vendor/level42/notjaxb-bundle/Annotation'); loadClasses(__DIR__ . '/vendor/level42/notjaxb-bundle/Exceptions'); loadClasses(__DIR__ . '/vendor/level42/notjaxb-bundle/Manager'); loadClasses(__DIR__ . '/vendor/level42/notjaxb-bundle/Mapping'); loadClasses(__DIR__ . '/example/entities'); |
Dans “example/entities”, on placera les objets à serialiser/désérialiser.
Conclusion
Une fois le principe compris, ce bundle me permet de faire du binding XML de manière efficace. La maintenance est simple, le code lisible et l’on reprends plaisirs à utiliser du XML pour stocker ou lire des informations. Idéalement, il faudrait pouvoir générer les objets avec annotations à partir d’un XSD ou d’un XML existant. C’est dans le “pipe”.