Cómo leer un archivo XML con namespaces

Estoy tratando de leer un xml el cual contiene namespaces, que pesadilla 🙁.

La verdad es bastante confuso para mi ese tema; he tratado de varias maneras pero hasta ahora sin éxito, por ahí en la web nos indicaron que lo mas conveniente es hacerlo mediante DOM, estuve leyendo la documentación en el sitio oficial de php, pero en serio que no logro ni siquiera ingresar al nodo principal.

Me recomendaron usar la librería SimpleXMLElement, pero es peor, siempre devuelve null.

¿Tu historia se parece a esta? No estás solo.

Personalmente, el formato XML me ha dado más dolores de cabeza de los que me gusta recordar… de hecho, creo que quien lo inventó debía ser un poco sádico.

Pero bueno… lamentablemente, aún hay quienes lo usan así que… será mejor intentar amigarse con él ¿no?

Voy a intentar ilustrar la solución a este problema usando un ejemplo real.

Imaginá que recibiste un texto como este:

<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope
    xmlns:s="http://www.w3.org/2003/05/soap-envelope"
    xmlns:a="http://www.w3.org/2005/08/addressing"
    xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    <s:Header>
        <a:Action s:mustUnderstand="1">http://wcf.dian.colombia/IWcfDianCustomerServices/GetStatusZipResponse</a:Action>
        <o:Security
            xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
            <u:Timestamp u:Id="_0">
                <u:Created>2022-07-16T21:22:34.242Z</u:Created>
                <u:Expires>2022-07-16T21:27:34.242Z</u:Expires>
            </u:Timestamp>
        </o:Security>
    </s:Header>
    <s:Body>
        <GetStatusZipResponse
            xmlns="http://wcf.dian.colombia">
            <GetStatusZipResult
                xmlns:b="http://schemas.datacontract.org/2004/07/DianResponse"
                xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
                <b:DianResponse>
                    <b:ErrorMessage
                        xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
                        <c:string>Regla: FAJ41, Notificación: El contenido de este elemento no corresponde al nombre y código valido.</c:string>
                        <c:string>Regla: FAJ73, Notificación: Estructura código no valida</c:string>
                    </b:ErrorMessage>
                    <b:IsValid>true</b:IsValid>
                    <b:StatusCode>00</b:StatusCode>
                    <b:StatusDescription>Procesado Correctamente.</b:StatusDescription>
                    <b:StatusMessage>La Factura electrónica SETP-99111223, ha sido autorizada.</b:StatusMessage>
                    <b:XmlBase64Bytes></b:XmlBase64Bytes>
                    <b:XmlBytes i:nil="true"/>
                    <b:XmlDocumentKey></b:XmlDocumentKey>
                    <b:XmlFileName>face_f0900056122003b023380</b:XmlFileName>
                </b:DianResponse>
            </GetStatusZipResult>
        </GetStatusZipResponse>
    </s:Body>
</s:Envelope>

Y necesitás obtener la información de las etiquetas:

  • <u:Created>
  • <u:Expires>
  • <b:ErrorMessage xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
  • <b:XmlDocumentKey>
  • <b:XmlFileName>

¿Cómo podrías hacer?

El elefante en la habitación

Este xml no es cualquier xml. Se trata de parte de una respuesta emitida por un webservice SOAP con lo cual, probablemente la mejor opción para resolver este problema sea evitarlo directamente, es decir, apoyarte en la clase SOAPClient.

De hecho, si lo miras con un poco más de detenimiento, se trata de una factura electrónica generada para Colombia. Si tu problema se parece a ese tal vez te convenga revisar este artículo.

Dicho esto, supongamos que, por alguna razón, esa opción no está disponible y no queda otra que interpretar a mano el xml.

Recargá el café y comencemos.

Usando xpath

Una primera aproximación a esto sería usar un código tipo:

<?php

$xml = new SimpleXMLElement($xmlString);
$created = $xml->xpath("//s:Envelope/s:Header/o:Security/u:Timestamp/u:Created");

print_r($created);

Pero al ejecutarlo vas a obtener algo como:

Warning: SimpleXMLElement::xpath(): Undefined namespace prefix in /app/index.php on line 3

¿Qué pasó?

El problema es que xpath no conoce el mapeo de prefijos (s:, o: y u:) a las respectivas definiciones de namespaces y, por lo tanto, no será capaz de interpretar el texto del XML.

Una solución posible para esto es ayudar un poco al pobre xpath.

¿Cómo?

Utilizando el método registerXPathNamespace del siguiente modo:

<?php

$xml = new SimpleXMLElement($xmlString);
$xml->registerXPathNamespace('s', "http://www.w3.org/2003/05/soap-envelope");
$xml->registerXPathNamespace('o', "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
$xml->registerXPathNamespace('u', "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
$created = $xml->xpath("//s:Envelope/s:Header/o:Security/u:Timestamp/u:Created");

print_r($created);

De esa forma, al ejecutar el programa verás:

Array
(
    [0] => SimpleXMLElement Object
        (
            [0] => 2022-07-16T21:22:34.242Z
        )

)

Y, a partir de ahí, si lo que efectivamente quieres es obtener la fecha puedes valerte de la clase DateTime o, su versión inmutable, DateTimeImmutable:

<?php

$xml = new SimpleXMLElement($xmlString);
$xml->registerXPathNamespace('s', 'http://www.w3.org/2003/05/soap-envelope');
$xml->registerXPathNamespace('o', "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd");
$xml->registerXPathNamespace('u', "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
$createdValue = $xml->xpath("//s:Envelope/s:Header/o:Security/u:Timestamp/u:Created")[0][0]->__toString();
try {
    $createdDate = new DateTimeImmutable($createdValue);
    echo "Created at: ".$createdDate->format("D M d H:i:s O").PHP_EOL;
} catch (DateMalformedStringException $e) {
    echo $e->getMessage().PHP_EOL;
}

Usando las propiedades del objeto SimpleXMLElement

En esta versión, en lugar de ir por el xpath lo haremos ingresando directamente a las propiedades del objeto SimpleXMLElement.

Cada uno de los elementos definidos en tags hijos del elemento principal (<s:envelope> en este caso) se convierte en forma automática, en una propiedad de sus elementos hijos.

Normalmente lo que haríamos sería:

<?php

$xml = new SimpleXMLElement($xmlString);
$createdValue = $xml
    ->children()
        ->Header
            ->children()
                ->Security
                    ->children()
                        ->Timestamp
                            ->children()
                                ->Created
                                    ->__toString();
try {
    $createdDate = new DateTimeImmutable($createdValue);
    echo "Created at: " . $createdDate->format("D M d H:i:s O") . PHP_EOL;
} catch (DateMalformedStringException $e) {
    echo $e->getMessage() . PHP_EOL;
}

Pero… nuevamente nos encontramos con un fallo:

Warning: Attempt to read property "Security" on null in /app/index.php on line 53

Fatal error: Uncaught Error: Call to a member function children() on null in /app/index.php:54

El problema aquí es que la identificación de esos hijos también depende del correcto mapeo del Namespace a su definición, con lo cual, es necesario indicar este hecho para que la navegación funcione

<?php

$xml = new SimpleXMLElement($xmlString);
$createdValue = $xml
    ->children("s", true)
        ->Header
            ->children("o", true)
                ->Security
                    ->children("u", true)
                        ->Timestamp
                            ->children("u", true)
                                ->Created
                                    ->__toString();
try {
    $createdDate = new DateTimeImmutable($createdValue);
    echo "Created at: " . $createdDate->format("D M d H:i:s O") . PHP_EOL;
} catch (DateMalformedStringException $e) {
    echo $e->getMessage() . PHP_EOL;
}

Namespaces y XML

En conclusión, trabajar con namespaces y XML no es tan complejo como puede parecer. Bueno, tan no complejo como XML permite al menos.

Sólo se trata de tener en cuenta cómo funcionan los namespaces y darle un poquito de guía a la librería SimpleXMLElement para que pueda hacer su magia como siempre.

mchojrin

Por mchojrin

Ayudo a desarrolladores PHP a afinar sus habilidades técnicas y avanzar en sus carreras

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.