j'avais rencontré quelques problèmes avec l'extension XML fournie avec PHP, aussi j'ai décidé de me coder mon parseur XML dans le cadre de mon projet OS. L'intéret de cette classe sans prétention (se content vraiment se faire strictement du parsing XML), est de pouvoir faire du parsing XML dans un environement où l'extension XML ne sera pas activée, ou bien tout simplement si l'extension XML de PHP ne vous convient pas (pour ma part j'ai eu des bugs liés à l'encodage avec l'extension XML de PHP).
Ainsi donc au lieu d'avoir un bon vieux
$this->xml_parser = xml_parser_create($encoding);
xml_parser_set_option($this->xml_parser, XML_OPTION_SKIP_WHITE, 0);
xml_parser_set_option($this->xml_parser, XML_OPTION_CASE_FOLDING, 0);
xml_set_character_data_handler($this->xml_parser, array(&$this, 'handle_cdata'));
xml_set_element_handler($this->xml_parser, array(&$this, 'handle_element_start'), array(&$this, 'handle_element_end'));
xml_parse($this->xml_parser, $this->xmldata);
avec la classe que je fourni vous devrez faire :
$xml = new Xml_regexp_parser();
$xml->obj =& $this;
$xml->open_handler = 'handle_element_start';
$xml->value_handler = 'handle_cdata';
$xml->close_handler = 'handle_element_end';
$result = $xml->parse($votre_chaine_XML);
votre ancien code restera ainsi totalement compatible, a un détail près : les handler appelé lors de l'ouverture / fermeture / valeur d'un tag ne prenne plus de premier argument (qui était avec l'extension XML l'objet du parseur XML).Voici le code de la classe :
class Xml_regexp_parser
{
// En faisant pointer cette propriété sur un objet, les handler seront appelés en tant que méthode de cet objet
var $obj = NULL;
// Fonction / méthode appelée lors de l'ouverture d'un tag
var $open_handler = NULL;
// Fonction / méthode appelée lors de la fermeture d'un tag
var $close_handler = NULL;
// Fonction / méthode appelée lors de la fermeture d'un tag, avec la valeur de celui ci
var $value_handler = NULL;
// Erreur lors du parsing
var $errstr = NULL;
/*
** Parse la chaîne de caractère XML
*/
function parse($str)
{
// Parse du header XML
if (preg_match('#^\s*<\?xml.*?\?>#si', $str, $m))
{
$str = preg_replace('#^\s*<\?xml.*?\?>#si', '', $str);
}
$stack = array();
$in_cdata = FALSE;
$last_offset = 0;
$value = '';
// Parse des différentes balises
preg_match_all('#<(/)?\s*([a-zA-Z0-9_\-]*?)(\s+(.*?))?\s*(/?)>\s*(<\!\[CDATA\[)?#si', $str, $m, PREG_OFFSET_CAPTURE);
$count = count($m[0]);
for ($i = 0; $i < $count; $i++)
{
// Longueur de la balise, offset de début et offset de fin de la chaîne
$length = strlen($m[0][$i][0]);
$curent_offset = $m[0][$i][1] + $length;
// Tag de la balise
$tag = $m[2][$i][0];
// Ouverture de balise ?
if (!$m[1][$i][0] || $m[5][$i][0])
{
// On ne prend pas en compte les tags dans un CDATA
if ($in_cdata)
{
continue ;
}
array_push($stack, $tag);
// Appel du handler d'ouverture du tag
if ($this->open_handler)
{
// Parse des attributs
preg_match_all('#([a-zA-Z0-9_\-]*?)="(.*?)"#si', $m[3][$i][0], $a, PREG_SET_ORDER);
$attrs = array();
foreach ($a AS $attr)
{
$attrs[$attr[1]] = $attr[2];
}
if ($this->obj)
{
$this->obj->{$this->open_handler}($tag, $attrs);
}
else
{
call_user_func($this->open_handler, $tag, $attrs);
}
}
// Début de CDATA ?
if ($m[6][$i] && $m[6][$i][0])
{
$in_cdata = TRUE;
}
}
// Fermeture de balise ?
if ($m[1][$i][0] || $m[5][$i][0])
{
$value = substr($str, $last_offset, $m[0][$i][1] - $last_offset);
// Fin de CDATA ?
if (substr($value, -2) == ']>')
{
$value = substr($value, 0, -3);
$in_cdata = FALSE;
}
// On ne prend pas en compte les tags dans un CDATA
if ($in_cdata)
{
continue ;
}
// Vérification de la fermeture du tag
$check = array_pop($stack);
if ($check != $tag)
{
$this->errstr = 'XML error : tag <' . $check . '> is different of <' . $tag . '>';
return (FALSE);
}
// Appel des handlers pour la fermeture des tags, et leur valeur
if ($this->obj && $this->value_handler)
{
$this->obj->{$this->value_handler}($value);
}
else if ($this->value_handler)
{
call_user_func($this->value_handler, $value);
}
if ($this->obj && $this->close_handler)
{
$this->obj->{$this->close_handler}($tag);
}
else if ($this->close_handler)
{
call_user_func($this->close_handler, $tag);
}
}
$last_offset = $curent_offset;
}
return (TRUE);
}
}
J'en profite pour joindre au message le parseur XML complet (c'est à dire l'analyseur de balise posté ci dessus, avec la classe qui s'occupe de mettre en forme correctement sous forme d'arbre le XML).
<?php
/*
** +---------------------------------------------------+
** | Name : ~/main/class/class_xml.php
** | Begin : 31/05/2007
** | Last : 31/05/2007
** | User : Genova
** | Project : Fire-Soft-Board 2 - Copyright FSB group
** | License : GPL v2.0
** +---------------------------------------------------+
*/
/*
** Parseur XML
*/
class Xml
{
// Contenu du code XML à parser
var $content;
// Racine de l'arbre XML
var $document;
// Pile utilisée par le parseur
var $stack = array();
/*
** Constructeur
*/
function Xml()
{
$this->document = new Xml_element();
}
/*
** Charge le contenu d'un fichier XML
** -----
** $filename :: Fichier XML à charger
*/
function load_file($filename)
{
if (!file_exists($filename))
{
die('Le fichier XML ' . $filename . ' n\'existe pas');
}
$this->content = file_get_contents($filename);
$this->parse();
}
/*
** Charge du code XML
** -----
** $content :: Contenu XML
*/
function load_content($content)
{
$this->content = $content;
$this->parse();
}
/*
** Parse du contenu XML
*/
function parse()
{
$xml = new Xml_regexp_parser();
$xml->obj =& $this;
$xml->open_handler = 'open_tag';
$xml->value_handler = 'value_tag';
$xml->close_handler = 'close_tag';
$result = $xml->parse($this->content);
if (!$result)
{
die($xml->errstr);
}
}
/*
** Callback appelé lors de l'ouverture d'un tag
*/
function open_tag($tag, $attr)
{
// Déplacement vers la référence de l'élément
$ref = &$this->document;
foreach ($this->stack AS $i => $item)
{
if ($i > 0)
{
$ref = &$ref->$item;
$ref = &$ref[count($ref) - 1];
}
}
// Création du nouvel élément
if (count($this->stack))
{
$new = $ref->createElement($tag);
foreach ($attr AS $k => $v)
{
$new->setAttribute($k, $v);
}
$new->__data['depth'] = count($this->stack);
$ref->appendChild($new);
}
else
{
$ref->setTagName($tag);
}
// Ajout du tag à la pile
array_push($this->stack, $tag);
}
/*
** Callback appelé lors de la fermeture d'un tag
*/
function close_tag($tag)
{
array_pop($this->stack);
}
/*
** Callback appelé lors de la capture de texte entre les tags
*/
function value_tag($text)
{
$ref = &$this->document;
foreach ($this->stack AS $i => $item)
{
if ($i > 0)
{
$ref = &$ref->$item;
$ref = &$ref[count($ref) - 1];
}
}
$ref->setData($text);
}
}
/*
** Représente une node de l'arbre XML
*/
class Xml_element
{
var $__data = array();
/*
** Constructeur
*/
function Xml_element()
{
$this->__data['name'] = 'newElement';
$this->__data['value'] = NULL;
$this->__data['attr'] = array();
$this->__data['depth'] = 0;
}
/*
** Créé un nouvel élément
** -----
** $name :: Nom du tag du nouvel élément
*/
function createElement($name)
{
$new = new Xml_element();
$new->setTagName($name);
return ($new);
}
/*
** Retourne la liste des attributs
*/
function attribute()
{
return ($this->__data['attr']);
}
/*
** Créé ou met à jour un attribut
** -----
** $name :: Nom de l'attribut
** $value :: Valeur de l'attribut
*/
function setAttribute($name, $value)
{
$this->__data['attr'][$name] = $value;
}
/*
** Retourne TRUE si l'attribut existe, sinon FALSE
** -----
** $name :: Nom de l'attribut
*/
function attributeExists($name)
{
return ((isset($this->__data['attr'][$name])) ? TRUE : FALSE);
}
/*
** Retourne la valeur d'un attribut
** -----
** $name :: Nom de l'attribut
*/
function getAttribute($name)
{
return (($this->attributeExists($name)) ? $this->__data['attr'][$name] : NULL);
}
/*
** Supprime un attribut
** -----
** $name :: Nom de l'attribut
*/
function deleteAttribute($name)
{
unset($this->__data['attr'][$name]);
}
/*
** Modifie le nom du tag
** -----
** $name :: Nom du tag
*/
function setTagName($name)
{
$this->__data['name'] = $name;
}
/*
** Retourne le nom du tag
*/
function getTagName()
{
return ($this->__data['name']);
}
/*
** Modifie la valeur du tag
** -----
** $value :: Chaîne de caractère
*/
function setData($value)
{
$this->__data['value'] = $value;
}
/*
** Retourne la valeur du tag
*/
function getData()
{
return ($this->__data['value']);
}
/*
** Ajoute un enfant
** -----
** $node :: Objet Xml_element
*/
function appendChild($node)
{
$name = $node->getTagName();
if (!isset($this->$name))
{
$this->$name = array();
}
$ref = &$this->$name;
$ref[] = $node;
}
/*
** Retourne la liste des enfants
*/
function children()
{
$children = array();
foreach ($this AS $property_name => $property_value)
{
if ($property_name != '__data')
{
$children[] = &$this->$property_name;
}
}
return ($children);
}
/*
** Retourne TRUE si l'enfant existe
** -----
** $name :: Nom de l'enfant
*/
function childExists($name)
{
return ((isset($this->$name)) ? TRUE : FALSE);
}
/*
** Retourne TRUE si l'élément a des enfants
*/
function hasChildren()
{
return ((count($this->children())) ? TRUE : FALSE);
}
/*
** Supprime l'enfant
** -----
** $name :: Nom de l'enfant
*/
function deleteChildren($name)
{
if ($this->childExists($name))
{
unset($this->$name);
}
}
/*
** Retourne l'arbre sous format XML
*/
function asXML()
{
$xml = str_repeat("\t", $this->__data['depth']) . '<' . $this->getTagName();
foreach ($this->attribute() AS $key => $value)
{
$xml .= ' ' . $key . '="' . htmlspecialchars($value) . '"';
}
$xml .= '>';
if ($this->hasChildren())
{
foreach ($this->children() AS $childs)
{
foreach ($childs AS $child)
{
$xml .= "\n" . $child->asXML();
}
}
$xml .= "\n" . str_repeat("\t", $this->__data['depth']) . '</' . $this->getTagName() . '>';
}
else
{
$xml .= '<![CDATA[' . $this->getData() . ']]></' . $this->getTagName() . '>';
}
return ($xml);
}
}
/*
** Parseur XML fait maison, afin d'éviter certains problèmes liés à l'encodage des caractères
*/
class Xml_regexp_parser
{
// En faisant pointer cette propriété sur un objet, les handler seront appelés en tant que méthode de cet objet
var $obj = NULL;
// Fonction / méthode appelée lors de l'ouverture d'un tag
var $open_handler = NULL;
// Fonction / méthode appelée lors de la fermeture d'un tag
var $close_handler = NULL;
// Fonction / méthode appelée lors de la fermeture d'un tag, avec la valeur de celui ci
var $value_handler = NULL;
// Erreur lors du parsing
var $errstr = NULL;
/*
** Parse la chaîne de caractère XML
*/
function parse($str)
{
// Parse du header XML
if (preg_match('#^\s*<\?xml.*?\?>#si', $str, $m))
{
$str = preg_replace('#^\s*<\?xml.*?\?>#si', '', $str);
}
$stack = array();
$in_cdata = FALSE;
$last_offset = 0;
$value = '';
// Parse des différentes balises
preg_match_all('#<(/)?\s*([a-zA-Z0-9_\-]*?)(\s+(.*?))?\s*(/?)>\s*(<\!\[CDATA\[)?#si', $str, $m, PREG_OFFSET_CAPTURE);
$count = count($m[0]);
for ($i = 0; $i < $count; $i++)
{
// Longueur de la balise, offset de début et offset de fin de la chaîne
$length = strlen($m[0][$i][0]);
$curent_offset = $m[0][$i][1] + $length;
// Tag de la balise
$tag = $m[2][$i][0];
// Ouverture de balise ?
if (!$m[1][$i][0] || $m[5][$i][0])
{
// On ne prend pas en compte les tags dans un CDATA
if ($in_cdata)
{
continue ;
}
array_push($stack, $tag);
// Appel du handler d'ouverture du tag
if ($this->open_handler)
{
// Parse des attributs
preg_match_all('#([a-zA-Z0-9_\-]*?)="(.*?)"#si', $m[3][$i][0], $a, PREG_SET_ORDER);
$attrs = array();
foreach ($a AS $attr)
{
$attrs[$attr[1]] = $attr[2];
}
if ($this->obj)
{
$this->obj->{$this->open_handler}($tag, $attrs);
}
else
{
call_user_func($this->open_handler, $tag, $attrs);
}
}
// Début de CDATA ?
if ($m[6][$i] && $m[6][$i][0])
{
$in_cdata = TRUE;
}
}
// Fermeture de balise ?
if ($m[1][$i][0] || $m[5][$i][0])
{
$value = substr($str, $last_offset, $m[0][$i][1] - $last_offset);
// Fin de CDATA ?
if (substr($value, -2) == ']>')
{
$value = substr($value, 0, -3);
$in_cdata = FALSE;
}
// On ne prend pas en compte les tags dans un CDATA
if ($in_cdata)
{
continue ;
}
// Vérification de la fermeture du tag
$check = array_pop($stack);
if ($check != $tag)
{
$this->errstr = 'XML error : tag <' . $check . '> is different of <' . $tag . '>';
return (FALSE);
}
// Appel des handlers pour la fermeture des tags, et leur valeur
if ($this->obj && $this->value_handler)
{
$this->obj->{$this->value_handler}($value);
}
else if ($this->value_handler)
{
call_user_func($this->value_handler, $value);
}
if ($this->obj && $this->close_handler)
{
$this->obj->{$this->close_handler}($tag);
}
else if ($this->close_handler)
{
call_user_func($this->close_handler, $tag);
}
}
$last_offset = $curent_offset;
}
return (TRUE);
}
}
?>
Avec un exemple d'utilisation :
<?php
include('class_xml.php');
$string = '<bibliotheque>
<roman type="policier">
<nom><![CDATA[Les 10 petits nègres]]></nom>
<description><![CDATA[bla bla bla bla]]></description>
<magasin name="paris ouest" />
<magasin name="paris sud" />
</roman>
<roman type="amour">
<nom><![CDATA[Titanic]]></nom>
<description><![CDATA[bla bla bla bla]]></description>
<magasin name="saint ouen" />
<magasin name="clichy" />
<magasin name="new york" />
</roman>
</bibliotheque>';
$xml = new Xml;
$xml->load_content($string);
foreach ($xml->document->roman AS $roman)
{
echo '<ul>';
echo '<li>Nom du roman : ' . $roman->nom[0]->getData() . '</li>';
echo '<li>Description du roman : ' . $roman->description[0]->getData() . '</li>';
echo '<li>Liste des magasins :<ul>';
foreach ($roman->magasin AS $magasin)
{
echo '<li>' . $magasin->getAttribute('name') . '</li>';
}
echo '</li></ul>';
echo '</ul>';
}
?>
J'ai essayé de la faire la plus intuitive possible. Il y a pas mal de méthodes assez utiles tel que children() qui retourne la liste des enfants, createElement() qui créé un nouvel élément, appendChild() qui permet d'ajouter l'élément créé dans l'arbre XML, etc ..Si ça peut servir, tant mieux.