Cómo enviar un archivo a un WebService SOAP desde PHP

Henry, un suscriptor del newsletter de Leeway Academy, me envía este mensaje:

Buenos dias.

Soy desarrollador php pero tengo este problema ya que este wsdl (https://test.sunat.gob.pe:444/ol-ad-itseida-ws/ReceptorService.htm) no me da mucha info.

Debo enviar un archivo xml tal cual, como archivo, con muchas etiquetas, en fin ya lo tengo resuelto. Una vez que tengo el ARCHIVO, cómo lo adjunto a este servicio para luego obtener el archivo de respuesta y leerlo?.

No veo en los parametros de SoapClient cómo adjuntarlo o la forma de enviarlo.

Si este es el formato correcto ( o no )

$xml='<.......... ';  (TODAS LAS ETIQUETAS QUE NECESITO ENVIAR)

$texto='<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ser="http://services.sigad.sunat.gob.pe">
   <soapenv:Header/>
   <soapenv:Body>
      <ser:recibirArchivo>
         <!--Optional:-->
         <numeroTransaccion>?</numeroTransaccion>
         <!--Optional:-->
         <informacionArchivo>$xml</informacionArchivo>
      </ser:recibirArchivo>
   </soapenv:Body>
</soapenv:Envelope>'

$texto = mb_convert_encoding($texto, "UTF-8");  //  CON Y SIN ESTA CONVERSION

$location_URL = 'https://test.sunat.gob.pe:444/ol-ad-itseida-ws/ReceptorService.htm?wsdl';
$action='urn:recibirArchivo';
$options = array(
// Stuff for development.
'location' => $location_URL,
// 'uri'      => 'https://test.sunat.gob.pe:444/ol-ad-itseida-ws/ReceptorService.htm',
'uri'  => 'https://test.sunat.gob.pe:444/ol-ad-itseida-ws/ReceptorService.htm?wsdl',
'username' => "XXXX",
'password' =>"YYYY",
'trace' => 1
);

AQUI ME ESTANCO

envio: $order_return = $client->__doRequest($texto,$location_URL,$action,0);

y recibo de respuesta

<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
   <S:Body>
      <ns0:Fault xmlns:ns0="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://www.w3.org/2003/05/soap-envelope">
         <faultcode>ns0:Server</faultcode>
         <faultstring>javax.xml.ws.soap.SOAPFaultException</faultstring>
      </ns0:Fault>
   </S:Body>
</S:Envelope>

Entonces no se la forma correcta de enviarlo.

Saludos y por favor ayuda.

Pasemos un poco en limpio el problema que tiene Henry.

Para empezar, el WSDL al que se refiere es este:

<?xml version='1.0' encoding='UTF-8'?>
<!-- Published by JAX-WS RI (http://jax-ws.java.net). RI's version is JAX-WS RI 2.3.0-b170407.2038 svn-revision#2eaca54d17a59d265c6fe886b7fd0027836c766c. -->
<!-- Generated by JAX-WS RI (http://jax-ws.java.net). RI's version is JAX-WS RI 2.3.0-b170407.2038 svn-revision#2eaca54d17a59d265c6fe886b7fd0027836c766c. -->
<definitions
    xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
    xmlns:wsp="http://www.w3.org/ns/ws-policy"
    xmlns:wsp1_2="http://schemas.xmlsoap.org/ws/2004/09/policy"
    xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata"
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns:tns="http://service.receptor.tecnologia.sunat.gob.pe/"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://service.receptor.tecnologia.sunat.gob.pe/" name="ReceptorService.htm">
    <import namespace="http://services.sigad.sunat.gob.pe" location="https://test.sunat.gob.pe:444/ol-ad-itseida-ws/ReceptorService.htm?wsdl=1"/>
    <binding
        xmlns:ns1="http://services.sigad.sunat.gob.pe" name="ReceptorWebServiceServiceImplPortBinding" type="ns1:ReceptorService.htm">
        <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
        <operation name="recibirArchivo">
            <soap:operation soapAction="urn:recibirArchivo"/>
            <input>
                <soap:body use="literal"/>
            </input>
            <output>
                <soap:body use="literal"/>
            </output>
            <fault name="Exception">
                <soap:fault name="Exception" use="literal"/>
            </fault>
        </operation>
        <operation name="realizarConsulta">
            <soap:operation soapAction="urn:realizarConsulta"/>
            <input>
                <soap:body use="literal"/>
            </input>
            <output>
                <soap:body use="literal"/>
            </output>
            <fault name="Exception">
                <soap:fault name="Exception" use="literal"/>
            </fault>
        </operation>
    </binding>
    <service name="ReceptorService.htm">
        <port name="ReceptorWebServiceServiceImplPort" binding="tns:ReceptorWebServiceServiceImplPortBinding">
            <soap:address location="https://test.sunat.gob.pe:444/ol-ad-itseida-ws/ReceptorService.htm"/>
        </port>
    </service>
</definitions>

Y según parece, lo que está intentando lograr Henry es enviar un archivo a un WebService de la agencia tributaria de Perú (Sunat).

Nunca antes me había tocado tratar con este WS, de modo que, antes de pasar al tema específico, realicé algunas pruebas.

Entender el WebService

El primer paso para resolver un problema como este es comprender qué tenemos del otro lado.

Nada mejor para empezar que usar una herramienta como cURL para descargar el archivo WSDL. Usando el comando curl https://test.sunat.gob.pe:444/ol-ad-itseida-ws/ReceptorService.htm\?wsdl obtuve:

curl: (60) SSL certificate problem: unable to get local issuer certificate<br>More details here: https://curl.haxx.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

No exactamente lo que esperaba… parece que me falta alguna verificación SSL.

Para saltearme este chequeo agrego el modificador -k al comando original, quedando curl -k https://test.sunat.gob.pe:444/ol-ad-itseida-ws/ReceptorService.htm\?wsdl y ahí sí puedo descargar el WSDL y usar un simple script basado en SoapClient para ver qué me ofrece este servicio de un modo algo más simple:

<?php

if ($argc < 2) {
    die('Specify the WSDL URI');
}

$client = new SoapClient($argv[1],
    [
        'stream_context' => stream_context_create([
                "ssl" => [
                    "verify_peer" => false,
                    "verify_peer_name" => false,
                ]]),
    ]
);
echo "Functions available:" . PHP_EOL;
echo "====================" . PHP_EOL;
print_r($client->__getFunctions());

Al ejecutar php describe_ws.php https://test.sunat.gob.pe:444/ol-ad-itseida-ws/ReceptorService.htm\?wsdl obtengo:

Functions available:
====================
Array
(
    [0] => recibirArchivoResponse recibirArchivo(recibirArchivo $parameters)
    [1] => realizarConsultaResponse realizarConsulta(realizarConsulta $parameters)
)

Es decir, tenemos dos métodos disponibles para invocar: recibirArchivo y realizarConsulta.

En particular, el que nos interesa en este caso es el primero.

Ahora sería interesante ver qué son exactamente los tipos recibirArchivo y recibirArchivoResponse.

Para ello le agregué un par de líneas al script:

echo "Types available:" . PHP_EOL;
echo "====================" . PHP_EOL;
print_r($client->__getTypes());

Y al correr nuevamente el scritp obtengo, además de lo que ya tenía:

Types available:
====================
Array
(
    [0] => struct Exception {
 string message;
}
    [1] => struct realizarConsulta {
 string parametrosConsulta;
}
    [2] => struct realizarConsultaResponse {
 base64Binary realizarConsultaResultado;
 respuestaConsultaBean respuesta;
}
    [3] => struct recibirArchivo {
 string numeroTransaccion;
 base64Binary informacionArchivo;
}
    [4] => struct recibirArchivoResponse {
 acuseRecibo recibirArchivoResultado;
}
    [5] => struct respuestaConsultaBean {
 string descripcion;
 string tipo;
}
    [6] => struct acuseRecibo {
 string anhoEnvio;
 string documentoEmisor;
 string fechaRecepcion;
 string hashDocumento;
 errorAcuse listaErrores;
 errorAcuse listaWarning;
 string numeroOrden;
 string ticketEnvio;
}
    [7] => struct errorAcuse {
 string codigo;
 string descripcion;
 string tipo;
}
)

Bien, ahora vemos entonces que, para invocar al servicio de recibirArchivo se requiere enviar un string (numeroTransaccion) y un dato de tipo base64Binary (informacionArchivo).

Esto ya me da alguna pista de lo que puede haber fallado en el caso de Henry… sigamos investigando.

Qué es base64Binary

Si vemos la definición en la especificación de SOAP encontramos lo siguiente:

[Definition:]  base64Binary represents Base64-encoded arbitrary binary data. The ·value space· of base64Binary is the set of finite-length sequences of binary octets. For base64Binary data the entire binary stream is encoded using the Base64 Alphabet in [RFC 2045].

Lo que esto significa en Español es que los datos del archivo deben ser enviados como una cadena codificada utilizando base64. De hecho, esto tiene bastante sentido ya que SOAP se monta sobre XML, un estándar basado en texto, de modo que algunos datos binarios podrían generar problemas.

Envío de un archivo binario a un WebService SOAP

Me temo que, como no tengo los datos de acceso al Sunat no voy a poder dar un ejemplo real pero dejaré uno que se parezca lo suficiente como para que pueda entenderse el mecanismo a utilizar.

Armé un pequeño programita en Java basado en lo que se muestra en este artículo.

Para correrlo uso este comando:

java1.8 -Dfile.encoding=UTF-8 -jar ./base64_server.jar

Y, a partir de ahí puedo consultar el WSDL a través de la URL http://localhost:9898/?wsdl, lo que me da:

<!--  Published by JAX-WS RI (http://jax-ws.java.net). RI's version is JAX-WS RI 2.2.9-b130926.1035 svn-revision#5f6196f2b90e9460065a4c2f4e30e065b245e51e.  -->
<!--  Generated by JAX-WS RI (http://jax-ws.java.net). RI's version is JAX-WS RI 2.2.9-b130926.1035 svn-revision#5f6196f2b90e9460065a4c2f4e30e065b245e51e.  -->
<definitions
    xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
    xmlns:wsp="http://www.w3.org/ns/ws-policy"
    xmlns:wsp1_2="http://schemas.xmlsoap.org/ws/2004/09/policy"
    xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata"
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
    xmlns:tns="http://soap.leewayweb.com/"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://soap.leewayweb.com/" name="FileTransfererImplService">
    <types>
        <xsd:schema>
            <xsd:import namespace="http://soap.leewayweb.com/" schemaLocation="http://localhost:9898/?xsd=1"/>
        </xsd:schema>
    </types>
    <message name="upload">
        <part name="parameters" element="tns:upload"/>
    </message>
    <message name="uploadResponse">
        <part name="parameters" element="tns:uploadResponse"/>
    </message>
    <message name="IOException">
        <part name="fault" element="tns:IOException"/>
    </message>
    <portType name="FileTransfererImpl">
        <operation name="upload">
            <input wsam:Action="http://soap.leewayweb.com/FileTransfererImpl/uploadRequest" message="tns:upload"/>
            <output wsam:Action="http://soap.leewayweb.com/FileTransfererImpl/uploadResponse" message="tns:uploadResponse"/>
            <fault message="tns:IOException" name="IOException" wsam:Action="http://soap.leewayweb.com/FileTransfererImpl/upload/Fault/IOException"/>
        </operation>
    </portType>
    <binding name="FileTransfererImplPortBinding" type="tns:FileTransfererImpl">
        <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
        <operation name="upload">
            <soap:operation soapAction=""/>
            <input>
                <soap:body use="literal"/>
            </input>
            <output>
                <soap:body use="literal"/>
            </output>
            <fault name="IOException">
                <soap:fault name="IOException" use="literal"/>
            </fault>
        </operation>
    </binding>
    <service name="FileTransfererImplService">
        <port name="FileTransfererImplPort" binding="tns:FileTransfererImplPortBinding">
            <soap:address location="http://localhost:9898/"/>
        </port>
    </service>
</definitions>

Y usando el mismo script que antes vemos que para invocarlo desde PHP tenemos los siguientes métodos:

Functions available:
====================
Array
(
    [0] => uploadResponse upload(upload $parameters)
)
Types available:
====================
Array
(
    [0] => struct upload {
 string arg0;
 base64Binary arg1;
}
    [1] => struct uploadResponse {
}
    [2] => struct IOException {
 string message;
}
)

Bastante parecido al caso que estoy analizando.

Pues bien, lo que resta entonces es ver cómo enviar un archivo binario al servidor. Todo el truco pasa por enviar el contenido del archivo como una cadena de caracteres (Del encoding base64 se encarga SoapClient).

Una función útil para esto es file_get_contents. Puede usarse de esta forma:

<?php

$client = new SoapClient('http://localhost:9898/?wsdl');

$contents = file_get_contents($argv[1]);
echo "Sending ".$contents.PHP_EOL;
try {
	$client->upload([
		'arg0' => basename($argv[1]), 
		'arg1' => $contents,
	]);
	echo "Sent!".PHP_EOL;
} catch (Exception $e) {
	var_dump($e);
}

Si se guarda el script como send_file.php puede ejecutarse con cualquier archivo binario mediante php send_file.php /ruta/al/archivo/binario y luego se podrá ver cómo se encuentra una copia en /ruta/al/programa/java/uploads.

Si querés probarlo en tu computadora podés descargar el archivo .jar de acá.

Advertencia: no usar con archivos grandes

Este mecanismo puede funcionar bien bajo el supuesto de que los archivos sean pequeños. Si este no fuera el caso, las limitaciones de HTTP pueden volverse un cuello de botella difícil de superar.

En todo caso, si buscas enviar archivos grandes no dejes de leer este artículo.

mchojrin

Por mchojrin

Ayudo a desarrolladores PHP a acceder mercados y clientes más sofisticados y exigentes

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