Etiqueta: xml

  • 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.

  • Cómo recorrer un archivo XML usando PHP

    Cómo recorrer un archivo XML usando PHP

    Como de costumbre, comencemos por ponernos de acuerdo en las definiciones.

    Qué es XML

    Las siglas XML remiten a eXtensible Markup Language (Lenguaje de etiquetas extendible).

    Se trata de texto estructurado mediante etiquetas (Palabras encerradas entre < y >):

    <utensilios>
      <tenedor/>
      <cuchillo/>
    </utensilios>
    

    Para qué sirve XML

    XML se inventó como un medio de intercambio de información entre sistemas a través de Internet. Al ser un formato basado en texto, era fácil aprovechar la infraestructura existente para comunicarse a través de HTTP.

    Hoy en día, XML es utilizado en muchas implementaciones de WebServices (Por ejemplo, las facturas electrónicas) y también para almacenar configuraciones (En el caso del lenguaje Java es muy común encontrar este tipo de archivos, en PHP no tanto).

    Cómo se procesa XML usando PHP

    Al tratarse de texto, un modo de procesar XML es, como cualquier otro texto… pueden usarse expresiones regulares u otro medio de análisis de texto para interpretar y/o generar las etiquetas.

    Claro que no es muy divertido que digamos…


    Para comprender completamente lo que viene a continuación se requieren conocimientos de Programación Orientada a Objetos con PHP. Si aún no lo tenés muy claro este curso te puede ayudar.


    Otro modo bastante más práctico es usar la biblioteca SimpleXML que viene con PHP.

    La clase principal de la biblioteca es SimpleXMLElement. Con esta clase se puede crear una estructura en memoria a partir de un texto XML y luego recorrerlo en forma sencilla.

    Ejemplo de lectura de XML con SimpleXMLElement

    El constructor de SimpleXMLElement recibe un texto XML:

    <?php
    $xml = new SimpleXMLElement( '<utensilios><tenedor/><cuchillo/></utensilios>' );

    Y a partir de ahí pueden realizarse diversas operaciones para recorrer los elementos.

    Una de ellas es buscar elementos explícitamente a través de su XPATH:

    $elementos = $xml->xpath('/utensilios/tenedor');

    Esta llamada dejará en el array $elementos todos los nodos tenedor que se encuentren bajo la clave utensilios (En este caso es sólo uno, pero eso SimpleXMLElement no lo sabe a priori).

    Si agregamos un print_r( $elementos ) veremos :

    Array
    (
        [0] => SimpleXMLElement Object
            (
            )
    
    )

    Es decir, un array con un único elemento… de tipo SimpleXMLElement, al que podemos nuevamente aplicarle todas las funciones que provee la clase.

    Ejemplo de recorrida de XML con SimpleXML

    Otra forma de recorrer el texto XML es a través del método children:

    <?php
    $xml = new SimpleXMLElement( '<utensilios><tenedor/><cuchillo/></utensilios>' );
    foreach ( $xml->children() as $child ) {
            print_r( $child );
    }

    Esta es la que deberías usar si no conoces exactamente la estructura del XML o si efectivamente tenés que recorrer el archivo completo.

    Cómo leer un archivo XML usando PHP

    Bueno, con lo que viste hasta ahora el paso que queda es realmente corto… se podría leer el contenido del archivo a una variable y pasarla al constructor de SimpleXMLElement:

    <?php
    $texto = file( 'archivo.xml' );
    $xml = new SimpleXMLElement( $texto );
    $elementos = $xml->xpath('/utensilios/tenedor');
    print_r( $elementos );

    O bien pasar directamente la ruta al archivo al constructor:

    <?php
    $xml = new SimpleXMLElement( 'archivo.xml' );
    $elementos = $xml->xpath('/utensilios/tenedor');
    print_r( $elementos );

    Y dejar que SimpleXML se encargue de interpretar si se trata de uno u otro.

    Conclusión

    Si todavía no te convencí de usar SimpleXML en lugar de hacer las cosas a mano, te invito a leer un poco más sobre el formato XML (Especialmente el manejo de namespaces).

    Ahora, si tenés que interactuar con un sistema que está implementado usando XML, bueno… no hay muchas opciones.

    Si, por el contrario, estás pensando en usar algún formato de texto para guardar configuración o incluso para intercambiar con otros sistemas, te sugiero darle una mirada a JSON o a YAML

  • Cómo enviar XML a un WebService con PHP

    Cómo enviar XML a un WebService con PHP

    Para empezar, debemos responder una pregunta escencial: ¿se trata de un WebService de tipo SOAP o uno de tipo REST?

    ¡La forma de enviarlo en uno u otro caso será muy diferente!

    Otra historia es cómo recibir XML a través de WebServices desarrollados usando PHP… tema para otro post 🙂

    Cómo enviar XML a un WebService SOAP con PHP

    Como siempre, para el caso de que se trate de un webservice de tipo SOAP, usaremos la clase SOAPClient

    Tomemos como ejemplo este Servicio Web que tiene un solo método disponible (CustomerSearch), el cual recibe XML y devuelve XML.

    Tenemos dos alternativas para generar el XML que queremos enviar:

    1. Escribirlo explícitamente (o «a mano»)
    2. Usar la clase SimpleXMLElement

    Si aún estás en dudas, no dudes más: usa la clase, te ahorrará una gran cantidad de dolores de cabeza.

    Con lo que la llamada se vería algo así como:

    <?php
    
    $url = 'https://secure.softwarekey.com/solo/webservices/XmlCustomerService.asmx?WSDL';
    $client = new SoapClient($url);
    
    $xmlr = new SimpleXMLElement("<CustomerSearch></CustomerSearch>");
    $xmlr->addChild('AuthorID', 1);
    $xmlr->addChild('UserID', 'mchojrin');
    $xmlr->addChild('UserPassword', '1234');
    $xmlr->addChild('Email', 'mauro.chojrin@leewayweb.com');
    
    $params = new stdClass();
    $params->xml = $xmlr->asXML(); // OJO: La propiedad xml es particular de este WebService, debes reemplazarla por el nombre del parámetro que espera recibir el servicio al que buscas conectarte
    
    $result = $client->CustomerSearchS($params);
    
    print_r($result);
    
    echo PHP_EOL;

    Si ejecutas este código te encontrarás con algo como:

    stdClass Object
    (
        [CustomerSearchSResult] => stdClass Object
            (
                [any] => <Customers xmlns=""><ResultCode>-1</ResultCode><ErrorMessage>Invalid Login</ErrorMessage></Customers>         
            ) 
    ) 

    Lo que seguramente te interese es lo que está dentro de la clave any, con lo cual, para obtenerlo podrías usar echo $result->CustomerSearchSResult->any; en lugar de print_r($result);. Aunque probablemente lo que quieras no sea mostrar el resultado explícitamente, si no procesarlo de alguna manera… ¿qué mejor que recurrir nuevamente a SimpleXMLElement?

    <?php
    
    $url = 'https://secure.softwarekey.com/solo/webservices/XmlCustomerService.asmx?WSDL';
    $client = new SoapClient($url);
    
    $xmlr = new SimpleXMLElement("<CustomerSearch></CustomerSearch>");
    $xmlr->addChild('AuthorID', 1);
    $xmlr->addChild('UserID', 'mchojrin');
    $xmlr->addChild('UserPassword', '1234');
    $xmlr->addChild('Email', 'mauro.chojrin@leewayweb.com');
    
    $params = new stdClass();
    $params->xml = $xmlr->asXML();
    
    $result = new SimpleXMLElement($client->CustomerSearchS($params)->CustomerSearchSResult->any);
    
    $r = current($result->xpath('/Customers/ResultCode'));
    
    if ( $r == '-1' ) {
            echo 'Fallo: '.$result->xpath('/Customers/ErrorMessage')[0];
    } else {
            echo 'Exito!';
    }
    
    echo PHP_EOL;

    Cómo enviar XML a un WebService REST con PHP

    En el caso de tratarse de un Servicio Web basado en REST todo es más fácil.

    Puedes usar cURL:

    <?php
    
    $server = 'http://www.leewayweb.com/miaplicacion';
    $headers = [
        "Content-type: text/xml",
        "Content-length: " . strlen($requestXML), "Connection: close",
    ];
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $server);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_TIMEOUT, 100);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $requestXML);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    $data = curl_exec($ch);
    
    if (curl_errno($ch)) {
        print curl_error($ch);
        echo "Algo fallo";
    } else {
        curl_close($ch);
    }

    O bien algo un poco más elaborado como Guzzle:

    <?php
    
    use GuzzleHttp\Client;
    use GuzzleHttp\Psr7\Request;
    
    $client = new Client();
    $request = new Request(
        'POST',
        $uri,
        [
            'Content-Type' => 'text/xml; charset=UTF8'
        ],
        $xml
    );
    
    echo $response->getBody();
    

    (Para que funcione este ejemplo hay que tener instalada la librería o incluirla, por ejemplo, usando composer.).