Leer XML en PHP: aunque JSON lo ha desplazado en las API, el XML sigue en todas partes, de las facturas electrónicas a los feeds RSS. PHP trae dos formas de leerlo: SimpleXML, para recorrerlo rápido, y DOM, para todo lo demás.
También te puede interesar
Usaremos este catálogo, con atributos y un espacio de nombres para los precios:
<?xml version="1.0" encoding="UTF-8"?>
<catalogo xmlns:p="https://ejemplo.com/precios">
<libro id="b1" idioma="es">
<titulo>Cien años de soledad</titulo>
<autor>Gabriel García Márquez</autor>
<p:precio moneda="MXN">389.00</p:precio>
</libro>
<libro id="b2" idioma="es">
<titulo>Rayuela</titulo>
<autor>Julio Cortázar</autor>
<p:precio moneda="MXN">299.50</p:precio>
</libro>
</catalogo>Lo sencillo: SimpleXML
<?php
declare(strict_types=1);
$ruta = sys_get_temp_dir() . '/catalogo.xml';
file_put_contents($ruta, <<<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<catalogo xmlns:p="https://ejemplo.com/precios">
<libro id="b1" idioma="es">
<titulo>Cien años de soledad</titulo>
<autor>Gabriel García Márquez</autor>
<p:precio moneda="MXN">389.00</p:precio>
</libro>
<libro id="b2" idioma="es">
<titulo>Rayuela</titulo>
<autor>Julio Cortázar</autor>
<p:precio moneda="MXN">299.50</p:precio>
</libro>
</catalogo>
XML);
$catalogo = simplexml_load_file($ruta, options: LIBXML_NONET);
foreach ($catalogo->libro as $libro) {
$precio = $libro->children('https://ejemplo.com/precios')->precio;
printf("%s, de %s (%s): %s %s\n",
$libro->titulo, $libro->autor, $libro['id'], $precio, $precio->attributes()['moneda']);
}
echo count($catalogo->libro), " libros\n";
$total = 0.0;
foreach ($catalogo->libro as $libro) {
$total += (float) $libro->children('https://ejemplo.com/precios')->precio;
}
echo "Total: ", number_format($total, 2), "\n";salidaCien años de soledad, de Gabriel García Márquez (b1): 389.00 MXN Rayuela, de Julio Cortázar (b2): 299.50 MXN 2 libros Total: 688.50
Con SimpleXML, cada elemento es una propiedad ($libro->titulo) y cada atributo, una clave de array ($libro['id']). Los elementos con espacio de nombres, como p:precio, se piden con children() y la URI del espacio, no con el prefijo. Lo que devuelve son objetos SimpleXMLElement: para hacer cuentas hay que convertirlos con (float) o (string).
Buscar con XPath
<?php
declare(strict_types=1);
$ruta = sys_get_temp_dir() . '/catalogo.xml';
file_put_contents($ruta, <<<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<catalogo xmlns:p="https://ejemplo.com/precios">
<libro id="b1" idioma="es">
<titulo>Cien años de soledad</titulo>
<autor>Gabriel García Márquez</autor>
<p:precio moneda="MXN">389.00</p:precio>
</libro>
<libro id="b2" idioma="es">
<titulo>Rayuela</titulo>
<autor>Julio Cortázar</autor>
<p:precio moneda="MXN">299.50</p:precio>
</libro>
</catalogo>
XML);
$catalogo = simplexml_load_file($ruta, options: LIBXML_NONET);
$catalogo->registerXPathNamespace('p', 'https://ejemplo.com/precios');
$baratos = $catalogo->xpath('//libro[p:precio < 300]/titulo');
echo "Menos de 300: ", implode(', ', array_map('strval', $baratos)), "\n";
[$autor] = $catalogo->xpath('//libro[@id="b1"]/autor');
echo "Autor de b1: $autor\n";salidaMenos de 300: Rayuela Autor de b1: Gabriel García Márquez
Lo completo: DOMDocument
<?php
declare(strict_types=1);
$ruta = sys_get_temp_dir() . '/catalogo.xml';
file_put_contents($ruta, <<<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<catalogo xmlns:p="https://ejemplo.com/precios">
<libro id="b1" idioma="es">
<titulo>Cien años de soledad</titulo>
<autor>Gabriel García Márquez</autor>
<p:precio moneda="MXN">389.00</p:precio>
</libro>
<libro id="b2" idioma="es">
<titulo>Rayuela</titulo>
<autor>Julio Cortázar</autor>
<p:precio moneda="MXN">299.50</p:precio>
</libro>
</catalogo>
XML);
$doc = new DOMDocument();
$doc->load($ruta, LIBXML_NONET);
foreach ($doc->getElementsByTagName('titulo') as $titulo) {
echo $titulo->textContent, "\n";
}
$nuevo = $doc->createElement('libro');
$nuevo->setAttribute('id', 'b3');
$nuevo->appendChild($doc->createElement('titulo', 'Pedro Páramo'));
$doc->documentElement->appendChild($nuevo);
echo $doc->getElementsByTagName('libro')->length, " libros tras añadir uno\n";salidaCien años de soledad Rayuela 3 libros tras añadir uno
DOM es más largo de escribir, pero permite todo: recorrer, modificar, crear y guardar documentos ($doc->save() o saveXML()). Es la opción cuando además de leer tienes que cambiar el XML.
LIBXML_NOENT ni LIBXML_DTDLOAD al leer XML que no controlas, y añade LIBXML_NONET, como en estos ejemplos, para que el analizador no haga peticiones a la red. Más en los fallos de seguridad típicos.Cuando el XML está mal
<?php
declare(strict_types=1);
libxml_use_internal_errors(true);
$xml = simplexml_load_string('<catalogo><libro>Sin cerrar</catalogo>', options: LIBXML_NONET);
var_dump($xml);
foreach (libxml_get_errors() as $error) {
printf("Línea %d: %s\n", $error->line, trim($error->message));
}
libxml_clear_errors();salidabool(false) Línea 1: Opening and ending tag mismatch: libro line 1 and catalogo Línea 1: Premature end of data in tag catalogo line 1
Por omisión, un XML mal formado llena la salida de avisos. Con libxml_use_internal_errors(true) se guardan en silencio, la función devuelve false y puedes recoger los errores con libxml_get_errors() para mostrarlos como quieras.
Resumen
| Para | Usa |
|---|---|
| Leer y recorrer | simplexml_load_file($ruta, options: LIBXML_NONET) |
| Atributos | $elemento['nombre'] |
| Espacios de nombres | ->children($uri) o registerXPathNamespace |
| Buscar | ->xpath('//libro[@id="b1"]') |
| Modificar o crear | DOMDocument |
| Errores sin avisos | libxml_use_internal_errors(true) |
Todo el código de esta página se ejecutó con PHP 8.5.12 en un contenedor limpio, sin acceso a la red, antes de publicarla; las salidas y los mensajes de error están copiados de esa ejecución.