Llevo ya un tiempo bastante liado con webservices a los que debo llamar con PHP y hoy me ha tocado lidiar con cabeceras SOAP. La verdad es que es un mundo bastante oscuro y me he encontrado con muchas trabas. Os contaré cuales y cómo las he solucionado, pero veamos primero algo de teoría.
Los servicios web se han convertido en el principal modo de intercambio de información entre aplicaciones independientemente de plataformas, sistemas operativos y lenguajes de programación. SOAP es uno de los protocolos sobre los que se realiza el intercambio de los datos y está basado en XML, de manera que la parte cliente interroga al servidor con un código XML en el formato adecuado y recibe la respuesta en otro XML. Para entender de qué estamos hablando veamos la estructura de una petición SOAP y su respuesta.
Llamada (request):
<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="com.xplota.ws">
<SOAP-ENV:Header>
<ns1:entity>
<Code>1</Code>
<Desc></Desc>
</ns1:entity>
<ns1:language>
<Code>1</Code>
<Desc></Desc>
</ns1:language>
<ns1:userId>
<Code>1</Code>
<Desc></Desc>
</ns1:userId>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
Respuesta (response):
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Header>
<Status xmlns="com.xplota.ws">
<Code>0</Code>
<Desc>Ok</Desc>
</Status>
</soap:Header>
<soap:Body>
</soap:Body>
</soap:Envelope>
Como se puede ver en los listados anteriores, tanto el request como el response constan de dos nodos XML, header y body. El que se utiliza habitualmente es el body (que yo he dejado en blanco pues no nos interesa en este momento) y es el que contendría tanto los parámetros que se envían al webservice en el request como los que devuelve en el response.
Enviando headers soap
En el caso que nos ocupa debía enviar determinados parámetros en el header y leer de allí los potenciales códigos de error si los hubiese habido. El envío, pese a ser una estructura en vez de un parámetro simple, fue sencillo, se define una clase con los parámetros adecuados y se le envía directamente. El motor de SOAP de PHP se encarga de la traducción. Veamos un caso práctico.
//definimos la clase para las cabeceras
class wsHeader
{
public $Code = 0;
public $Desc = '';
public function __construct($code, $desc){
$this->Code=$code;
$this->Desc=$desc;
}
}
//instanciamos el cliente soap
$par=array();
$client = new SoapClient("http://midominio.com/ws?wsdl", $par);
//añadimos las cabeceras a las peticiones
$headers=array();
$headers[] = new SoapHeader("com.xplota.ws", 'entity', new wsHeader(1, ''));
$headers[] = new SoapHeader("com.xplota.ws", 'language', new wsHeader(1, ''));
$headers[] = new SoapHeader("com.xplota.ws", 'userId', new wsHeader(1, ''));
$client->__setSoapHeaders($headers);
//lanzamos la llamada al metodo del ws
$result = $client->TuMetodo($parametros);
Como veis es bastante sencillo de entender. Al añadir una cabecera hay que indicarle el namespace al que pertenece para que el motor SOAP sepa como tratarla, se le da un nombre y el objeto que la contiene.
Con esto hemos solucionado la parte del envío de nuestras cabeceras SOAP y tendremos un request como indicábamos en el primer XML.
Recibiendo headers SOAP
Ahora resulta que el método de nuestro webservice nos responde con otras cabeceras que debemos saber interpretar según el XML de response del segundo listado. Pues tenemos un problema y muy gordo. No hay forma de obtener estas cabeceras, el motor SOAP de PHP sólo devuelve el body, nunca los headers.
Según el manual de PHP el método __soapCall del cliente SOAP permite definir un array en el que se devolverán estas cabeceras, pero no fui capaz de hacer funcionar la invocación de un método del webservice con esta sintaxis mientras que invocándolos directamente en el cliente (cómo la documentación indica que se puede hacer) sí que me funcionaba perfectamente. Es decir, la teoría dice que con el primer método puedo recibir las cabeceras pero no me funcionó mientras que el segundo me funcionaba pero no me devuelve las cabeceras ni hay ningún método para recuperarlas.
Tras pelearme mucho con las funciones SOAP e investigar todavía más no llegué a ninguna conclusión, es como si no le hubiese pasado a nadie, no encontré absolutamente nada útil. Sólo me quedaba una solución, hacer mi propia clase SOAP a partir de la original y procesar el XML del response a mano para obtener los datos que necesitaba. Dicho y hecho. Veamos la solución.
Primero creo mi propia clase de SOAP y compruebo si voy a poder hacer lo que quiero.
class XSoapClient extends SoapClient{
public function __construct($wsdl, $options){
parent::__construct($wsdl, $options);
}
public function __doRequest($request, $location, $action, $version){
$response=parent::__doRequest($request, $location, $action, $version);
return $response;
}
}
$client = new XSoapClient("http://midominio.com/ws?wsdl", $par);
Parece que voy a tener suerte, si pruebo este nuevo cliente SOAP funciona perfectamente, pero además si compruebo el contenido de $response veo que contiene íntegramente el XML de la respuesta del webservice. Cómo veis lo único que cambia al instanciarlo es que le paso el nombre de la nueva clase. Buen comienzo, si juego bien mis cartas podré sacar las cabeceras en el método __doRequest 🙂 .
Tratemos pues ese XML para obtener lo que buscamos. Gracias a las funciones DOM y XPATH de PHP será muy sencillo. Este es el resultado final de mi cliente SOAP con recuperación de cabeceras:
class XSoapClient extends SoapClient
{
private $responseHeaders = array();
public function __construct($wsdl, $options){
parent::__construct($wsdl, $options);
}
public function __doRequest($request, $location, $action, $version){
$response=parent::__doRequest($request, $location, $action, $version);
$dom = new DOMDocument;
$dom->loadXML($response, LIBXML_NOWARNING);
$path = new DOMXPath($dom);
$path->registerNamespace('soap', 'http://schemas.xmlsoap.org/soap/envelope/');
$xml = $path->query('//soap:Header/*');
$this->responseHeaders=$this->headers2array($xml);
return $response;
}
public function getResponseHeaders(){
return $this->responseHeaders;
}
private function headers2array($response){
$headers=array();
foreach ($response as $node) {
if($node->hasChildNodes()){
$headers[$node->nodeName]=$this->headers2array($node->childNodes);
}else{
$headers[$node->nodeName]=$node->nodeValue;
}
}
return $headers;
}
}
$client = new XSoapClient("http://midominio.com/ws?wsdl", $par);
$result = $client->TuMetodo($parametros);
$soapheaders=$client->getResponseHeaders();
Problema solucionado y de manera bastante elegante. Si alguien sabe cómo conseguir las cabeceras sin montar todo este lio que me lo cuente por favor.