Archivo de la etiqueta: webservice

Mi primer webservice en PHP (chispas)

Tras mucho tiempo consumiendo webservices de otros me ha tocado crear mi primer servidor SOAP en PHP y, la verdad, me ha parecido realmente sencillo e intuitivo. Creas una clase con los métodos que vas a exponer en el ws y se crea automáticamente el servicio sobre ellos, tan sencillo como eso.

<?php
$wsdl="miclase.wsdl";
$soap = new SoapServer($wsdl);
$soap->setClass('MiClase');
$soap->handle();

//clase que gestiona el ws
class MiClase {
    public function MiClase(){
      //tu código
    }

    /**
     *
     * @param string $email
     * @return string
     */
    public function is_email_available($email){
        //tu codigo...
        return "OK";
    }
    /**
    *
    * @param string $phone
    * @param string $email
    * @return string
    */
    public function register_user($phone, $email){
       //tu codigo...
       return "OK";
    }
    /**
    *
    * @param string $phone
    * @return string
    */
    public function downgrade_user($phone){
       //tu codigo...
       return "OK";
    }
}
?>

Con esto se crea automáticamente nuestro webservice con los tres métodos públicos. Pero espera, falta algo, arriba de todo defines un «miclase.wsdl«. ¿Qué es eso? ¿De dónde sale?

En efecto, ese es el principal problema al crear un webservice SOAP con PHP, no se genera el WSDL automáticamente sino que hay que escribirlo ¡a mano!. Para solucionarlo tenemos la librería PHP WSDL Generator a la que únicamente debemos pasarle la clase de la que queremos extraer el WSDL y lo hace por nosotros :). Para que todo funciona bien es necesario que los métodos de nuestra clase estén bien documentados tal y como aparecen en el ejemplo anterior, de esta manera WSDL Generator sabrá configurar los tipos de datos de los parámetros de entrada y salida de los métodos.

Veamos un ejemplo:

<?php
require_once("wsdl2php/WSDLCreator.php");
$test = new WSDLCreator("miclase", "http://ws.tudominio.com/wsdl");
$test->addFile("miclase.php");
$test->setClassesGeneralURL("http://tudominio.com");
$test->addURLToClass("MiClase", "http://ws.tudominio.com/miclase.php");
$test->ignoreMethod(array("MiClase"=>"MiClase"));
$test->createWSDL();
$test->saveWSDL(dirname(__FILE__)."/miclase.wsdl", false);
?>

Este pequeño código nos generará el archivo WSDL de nuestro webservice. Como veis simplemente le indicamos el archivo con nuestra clase (el que escribimos anteriormente), la clase que queremos mapear con la URL del webservice (el endpoint) y, además, le indicamos que ignore el constructor de la clase ya que no será un método de nuestro webservice. Eso es todo.

Si ahora probamos el servicio web, por ejemplo desde el Web Service Explorer de Eclipse:

Tras darle la ruta del wsdl, http://ws.tudominio.com/miclase.php?wsdl, veremos los tres métodos que hemos expuesto y podremos probarlos y utilizarlos.

Nunca había tenido la necesidad de crear un servidor SOAP pero ha sido realmente sencillo. Ahora estoy buscando la manera de devolver tipos de datos complejos, pero eso será en el próximo capítulo :P.

Segmentation fault al instanciar un webservice WCF de .NET desde PHP

Recientemente nos hemos encontrado con un problema que nos ha tenido varios días bloqueados hasta conseguir averiguar el origen. Llevamos mucho tiempo utilizando webservices programados en .NET desde aplicaciones PHP sin ningún problema, pero esta vez estaba hecho con la nueva tecnología WCF (Windows Communication Foundation) de Microsoft .NET 3.5. El problema era que en cuanto lo subimos a producción la aplicación PHP devolvía un pantallazo en blanco sin más información. Analizando los logs vimos que el proceso de Apache provocaba un Segmentation Fault con lo que no llegábamos a ver ninguna excepción.

Tras muchas pruebas conseguimos aislar el error en la línea de código que instanciaba el nuevo webservice, si eliminábamos esa parte todo funcionaba correctamente.

$client=new SoapClient("http://wcf.tudominio.com/webservice/ws.svc?wsdl");

Lo más curioso es que en los entornos de preproducción sí que funcionaba, no entendíamos nada. Analizando las máquinas de los distintos entornos todas eran idénticas en cuanto a versiones y configuración excepto algunos parámetros SOAP para PHP, en la máquina de producción tienen la caché de wsdl activada mientras que en los demás está desactivada.

soap.wsdl_cache_enabled = 1
soap.wsdl_cache_dir = /tmp/
soap.wsdl_cache_ttl = 7200
soap.wsdl_cache_limit = 50

No puede ser que el error sea el cacheo. Pues sí, lo es, en cuanto desactivamos la caché del servidor de producción todo comenzó a funcionar correctamente.

Perfecto, pero la caché tiene que estar activada, la aplicación hace uso de unos 35 webservices, si para cada instancia de cada uno de ellos hay que cargar previamente el wsdl, el rendimiento cae por los suelos, es imprescindible.

Pues nada, solución increíblemente cutre:

ini_set('soap.wsdl_cache_enabled', '0');
ini_set('soap.wsdl_cache_ttl', '0');
ini_set('soap.wsdl_cache', '0');

$client = new SoapClient("http://wcf.tudominio.com/webservice/ws.svc?wsdl");

ini_set('soap.wsdl_cache_enabled', '1');
ini_set('soap.wsdl_cache_ttl', '7200');
ini_set('soap.wsdl_cache', '3');

Así es, desactivamos la caché antes de instanciar sólo este webservice y la volvemos a activar después. No hemos encontrado otra manera de solucionarlo ni hemos encontrado ninguna referencia de alguien que haya sufrido el mismo problema. La solución es mala, muy mala, no deja de ser un apaño, pero funciona y nos permite salir del paso hasta que sepamos por qué ocurre.

Monitorizando aplicaciones web con rrdtool

Vamos con uno de esos artículos que le gustan a Álvaro basados en experiencias del trabajo.

En uno de los proyectos en los que estamos trabajando actualmente y del que ya os he hablado, teníamos un problema de rendimiento en una aplicación web, pero no teníamos localizado el punto donde se nos iba el tiempo. Incluso había momentos que la aplicación iba bien pero en otros era lamentable. Se nos ocurrió, entonces, hacer un sencillo sistema de monitorización de los distintos puntos sensibles que manejábamos y crear con ellos una serie de gráficas que nos permitiese hacer un seguimiento visual de los potenciales problemas.

Nuestra aplicación se basa en llamadas a distintos webservices a partir de las cuales se genera un XML al que se le aplican transformaciones XSL para obtener el código html que se muestra al usuario. Algunas de esas llamadas se guardan en la sesión del usuario, con lo que en sucesivos accesos a la aplicación el rendimiento es infinitamente mejor, así que necesitábamos que el control se hiciese con sesiones limpias para que no se nos desvirtuasen las gráficas.

Nuestro sistema consiste en generar por un lado, en la aplicación web, una serie de datos estadísticos que nos ayuden a buscar los puntos conflictivos. Estos datos se insertarán periódicamente en una base de datos rrd (round robin database) que finalmente utilizaremos para generar las gráficas.

Preparando la aplicación web

El primer paso es, entonces, modificar el código de la aplicación para que vaya generando los tiempos de respuesta en los distintos puntos que queremos controlar. Para facilitar las cosas decidimos que cada vez que se pase un parámetro predefinido en la url, en vez de mostrar la página html correspondiente mostraremos las estadísticas que después controlaremos. En nuestro caso decidimos que la url de estadísticas fuese:

http://tudominio.com/index.php?stats

Es decir, añadiendo el parámetro stats a cualquiera de las urls del site obtendremos los tiempos de respuesta que necesitamos.

Tenemos una función que genera el tiempo exacto del servidor cada vez que se llama, así que la utilizaremos después de ejecutar cada uno de los webservices y guardaremos esos datos. En mi caso tengo que obtener ocho puntos de control además del tiempo de inicio y el tiempo final, algo así:

function getTime() {
 $mtime2 = explode(" ", microtime());
 $endtime = $mtime2[0] + $mtime2[1];
 return $endtime;
}
$isStats=isset($_GET['stats']);
if($isStats)
    $time_inicio=getTime();
....
if($isStats)
    $time_ws1=getTime();
.....
if($isStats)
    $time_ws2=getTime();
....
if($isStats)
    $time_ws3=getTime();
....
if($isStats)
    $time_ws4=getTime();
....
if($isStats)
    $time_ws5=getTime();
....
if($isStats)
    $time_ws6=getTime();
....
if($isStats)
    $time_ws7=getTime();
....
if($isStats)
    $time_ws8=getTime();
....
if($isStats)
    $time_final=getTime();
......

Finalmente devolvemos una línea con todos los valores intermedios, es decir, para cada indicador sería el valor del tiempo de ese indicador menos el del anterior, esto nos dará el valor real del tiempo que ha pasado entre el indicador anterior y el actual. Para facilitar la posterior integración de estos valores con rrdtool lo que haremos será generarlos ya en el formato adecuado, separando cada valor con «:».

if(isset($_GET['stats'])){
	echo number_format($time_ws1-$time_inicio, 7).":".
			number_format($time_ws2-$time_ws1, 7).":".
			number_format($time_ws3-$time_ws2, 7).":".
			number_format($time_ws4-$time_ws3, 7).":".
			number_format($time_ws5-$time_ws4, 7).":".
			number_format($time_ws6-$time_ws5, 7).":".
			number_format($time_ws7-$time_ws6, 7).":".
			number_format($time_ws8-$time_ws7, 7).":".
			number_format($time_final-$time_inicio, 7);
}

Si ahora llamamos a nuestra url obtendremos los valores de nuestros indicadores. El último valor nos devolverá el tiempo completo de ejecución del script entre los valores $time_inicio y $time final.

0.0281749:0.5443010:0.3132501:2.9015441:0.0000241:0.6517198:5.5171580:0.0000379:10.0677590

Vemos cómo esta llamada ha tardado diez segundos, de los cuales 5,5 corresponden al indicador ws7.

Tenemos el primer paso preparado, vamos ahora a crear la base de datos donde almacenaremos toda la información estadística de la url. Tendremos una tarea que se ejecutará cada minuto y que recuperará los valores en ese momento de los identificadores que se han definido.

Creando la base de datos

Para guardar la información he escogido rrd, el mismo que utilizan aplicaciones como mrtg o Cacti y el utilizado en la mayoría de sistemas de monitorización. Según reza en su propia web, es el sistema estándar para gráficas y logs de series de datos temporales.

Para que todo vaya bien nuestra base de datos rrd debe tener la misma cantidad de fuentes de datos que el sistema de estadísticas, nueve en este caso, los ocho puntos de control más el total. Además tenemos que pensar qué graficas vamos a crear. Yo quiero cuatro: última hora, últimas seis horas, últimas 24 horas y última semana. No necesito hacer un sistema con histórico avanzado, lo que quiero en este caso es controlar el rendimiento, así que no necesitaré gráficas mensuales ni anuales, pero se podrían generar de igual modo.

Para crear nuestro archivo rrd ejecutamos el comando siguiente:

rrdtool create estadisticas.rrd --step 60
DS:ws1:GAUGE:120:0:U
DS:ws2:GAUGE:120:0:U
DS:ws3:GAUGE:120:0:U
DS:ws4:GAUGE:120:0:U
DS:ws5:GAUGE:120:0:U
DS:ws6:GAUGE:120:0:U
DS:ws7:GAUGE:120:0:U
DS:ws8:GAUGE:120:0:U
DS:total:GAUGE:120:0:U 

RRA:AVERAGE:0.5:1:60
RRA:AVERAGE:0.5:1:360
RRA:AVERAGE:0.5:5:144
RRA:AVERAGE:0.5:10:202
RRA:MAX:0.5:1:60
RRA:MAX:0.5:1:360
RRA:MAX:0.5:5:144
RRA:MAX:0.5:10:202
RRA:MIN:0.5:1:60
RRA:MIN:0.5:1:360
RRA:MIN:0.5:5:144
RRA:MIN:0.5:10:202

Vamos a explicarlo un poco.

  • –step=60: indica que los datos los actualizaremos cada 60 segundos. Nuestro sistema guardará estadísticas cada minuto.
  • DS:ws1:GAUGE:120:0:U: Fuentes de datos, hay una línea de este tipo para cada valor que vamos a mantener.
    1. wsX es el nombre que tendrá esa fuente.
    2. GAUGE es el tipo de fuente de datos, puedes consultar la documentación completa para tener más idea.
    3. 120 es el parámetro heartbeat, tiempo máximo entre dos actualizaciones antes de que se asuma que es desconocido. He puesto el doble de step, si pasado ese tiempo no hay una actualización es que se ha perdido por cualquier razón.
    4. Los últimos datos son los valores mínimo y máximo que habrá en la fuente de datos. En nuestro caso el mínimo será cero y el máximo no lo sabemos, le ponemos U de unknown.
  • RRA:AVERAGE:0.5:1:60: Round robin archives. El objetivo de una rrd es guardar datos en los rra que mantienen valores y estadísticas para cada una de las fuentes de datos definidas.
    1. AVERAGE/MAX/MIN/LAST: la función utilizada para consolidar los datos. Como su propio nombre indica son la media, máximo, mínimo y el último valor.
    2. 0.5; el xff (xfiles factor). De 0 a 1, indica el número de datos UNKNOWN que se permiten por intervalo de consolidación.
    3. 1: define el número de datos utilizados para crear un intervalo de consolidación.
    4. 60: Cuantos datos se mantienen en cada RRA. Esto depende de la gráfica que vamos a generar y se relaciona con el dato anterior.

La última parte es la más complicada de comprender. Intentaré explicarlo lo mejor que pueda para que se entienda. En nuestro caso tomamos un dato cada 60 segundos y queremos tener la gráfica de la última hora (60 segundos x 60 minutos, 3600 segundos). Necesitamos, por tanto, 60 puntos de control (3600/60 segundos de intervalo), utilizaremos cada uno de los datos que se toman. Esto es lo que indica la creación RRA:AVERAGE:0.5:1:60, queremos utilizar 60 puntos de control tomados cada 60 segundos. Este es bastante fácil, veamos el siguiente.

RRA:MIN:0.5:1:360

Ahora estamos guardando 360 puntos generados cada 60 segundos, es decir, cubrimos 60×360=21600 segundos=6 horas. Era sencillo.

RRA:AVERAGE:0.5:5:144

En la siguiente gráfica queremos obtener datos de las últimas doce horas (12 horas x 60 minutos x 60 segundos, 43200 segundos). Ahora ya no necesitamos saber el comportamiento exacto en cada momento sino que necesitamos una visión global de lo que ha ocurrido durante esas doce horas, tomamos, por tanto, un punto de control cada cinco datos de registro, es decir, estamos manteniendo un dato cada 5×60 segundos, 300 segundos. Necesitaremos por tanto 43200/300=144 valores para obtener nuestra gráfica de las últimas 12 horas

RRA:MIN:0.5:10:202

La última gráfica es semanal, 7x24x60x60=604800 segundos. Acepto que con tener un valor cada 10 minutos será suficiente, tendré entonces un valor cada 10×300=3000 segundos. 604800/3000=~202.

Es algo difícil de entender al principio, pero una vez lo piensas, todo tiene sentido.

Guardando las estadísticas

Tenemos ya nuestra base de datos preparada, creemos ahora la tarea que guardará en este archivo rrd los datos estadísticos. En mi caso es una tarea PHP que lo hace todo muy sencillo.

tarea.php

$url="http://dominio.com/?stats";
$cont=file_get_contents($url);
system("rrdtool update estadisticas.rrd N:".$cont);

Añadimos la tarea al cron para que se ejecute cada 60 seguntos.

*/1 * * * * /usr/bin/php /path/to/tarea.php > /dev/null 2>&1

¡Y ya está! Nuestra base de datos de estadísticas se está alimentando automáticamente.

Creando las gráficas

Lo más importante a fin de cuentas, lo que realmente vamos a utilizar. Rrdtool viene con un script de ejemplo que debes colocar en el directorio cgi-bin de tu servidor web y realizar los ajustes adecuados. Yo he puesto como ejemplo la gráfica de la última hora, las demás se harían igual variando el parámetro «–start» a los segundos anteriores que queremos mostrar.

/cgi-bin/stats.cgi

#!/usr/bin/rrdcgi

Tu Dominio - Home Stats

<RRD::GRAPH /usr1/www/www.genteirc.com/htdocs/imagenes/daily-visitas.png --imginfo '<img src="http://tudominio.com/%s" alt="" width="%lu" height="%lu" />' --start -3600 --end -0 -h 400 -w 600 --vertical-label="Tiempo" --title="Last Hour" -a PNG -l 0 DEF:ws1=/path/to/estadisticas.rrd:ws1:AVERAGE LINE1:ws1#00FF00:WebSer1t GPRINT:ws1:AVERAGE:"Media : %.4lft" GPRINT:ws1:MAX:"Max : %.4lft" GPRINT:ws1:MIN:"Min : %.4lfl" DEF:ws2=/path/to/estadisticas.rrd:ws2:AVERAGE LINE1:ws2#800000:WebSer2t GPRINT:ws2:AVERAGE:"Media : %.4lft" GPRINT:ws2:MAX:"Max : %.4lft" GPRINT:ws2:MIN:"Min : %.4lfl" DEF:ws3=/path/to/estadisticas.rrd:ws3:AVERAGE LINE1:ws3#FF8000:WebSer3t GPRINT:ws3:AVERAGE:"Media : %.4lft" GPRINT:ws3:MAX:"Max : %.4lft" GPRINT:ws3:MIN:"Min : %.4lfl" DEF:ws4=/path/to/estadisticas.rrd:ws4:AVERAGE LINE1:ws4#400080:WebSer4t GPRINT:ws4:AVERAGE:"Media : %.4lft" GPRINT:ws4:MAX:"Max : %.4lft" GPRINT:ws4:MIN:"Min : %.4lfl" DEF:ws5=/path/to/estadisticas.rrd:ws5:AVERAGE LINE1:ws5#0000FF:WebSer5t GPRINT:ws5:AVERAGE:"Media : %.4lft" GPRINT:ws5:MAX:"Max : %.4lft" GPRINT:ws5:MIN:"Min : %.4lfl" DEF:ws6=/path/to/estadisticas.rrd:ws6:AVERAGE LINE1:ws6a#00FFF0:WebSer6t GPRINT:ws6a:AVERAGE:"Media : %.4lft" GPRINT:ws6:MAX:"Max : %.4lft" GPRINT:ws6:MIN:"Min : %.4lfl" DEF:ws7=/path/to/estadisticas.rrd:ws7:AVERAGE LINE1:ws7#FF00FF:WebSer7t GPRINT:ws7:AVERAGE:"Media : %.4lft" GPRINT:ws7:MAX:"Max : %.4lft" GPRINT:ws7:MIN:"Min : %.4lfl" DEF:ws8=/path/to/estadisticas.rrd:ws8:AVERAGE LINE1:ws8#FFFF00:WebSer8t GPRINT:ws8:AVERAGE:"Media : %.4lft" GPRINT:ws8:MAX:"Max : %.4lft" GPRINT:ws8:MIN:"Min : %.4lfl" DEF:total=/path/to/estadisticas.rrd:total:AVERAGE LINE2:total#FF0000:'Total t' GPRINT:total:AVERAGE:"Media : %.4lft" GPRINT:total:MAX:"Max : %.4lft" GPRINT:total:MIN:"Min : %.4lfl" >

Para cada línea que queremos mostrar en la gráfica le indicamos de qué archivo rrd saldrán los datos y la fuente (DS) que vamos a utilizar. Añadimos además como leyenda los datos de media, máximo y mínimo de cada fuente, de manera que en una sola gráfica tenemos toda la información necesaria. Si ahora vas a la url donde has dejado el cgi tendrás:

http://tudominio.com/cgi-bin/stats.cgi

stats

Y tu te preguntarás, con lo pesado que te pones con Cacti, ¿por qué no meterla ahí también? Buena pregunta :P. Si tengo un ratito os enseño cómo integrarlo también con Cacti :).

Me he pasado con este artículo, no se lo va a leer nadie :P.

Webservices: Tratando con cabeceras SOAP en PHP (2)

Tras el artículo anterior dónde explicaba cómo leer las cabeceras en una respuesta SOAP, he descubierto cómo hacer funcionar el método estándar, seguro que a más de uno le viene bien saberlo.

El problema era que con __soapCall no había manera de que me funcionase la llamada, independientemente de recibir las cabeceras de la respuesta. Ahora sé porqué. El webservice al que estaba llamando está hecho en .NET y parece ser que hay que llamarlo de distinta forma que si se hace invocando al método directamente 😐 .

Si lo llamamos directamente hacemos:

$result = $client->TuMetodo($parametros);

Si lo llamamos con __soapCall haremos:

$result = $client->__soapCall("TuMetodo", array("parameters"=>$parametros), NULL, $reqheaders, $resheaders);

¿Veis la diferencia?

De la segunda forma hay que pasar los parámetros de entrada del método cómo un sólo parámetro «parameters«, es decir, el mismo array que teníamos con la primera manera pero asignándolo a «parameters«.

Eso es todo. Ahora ya funciona y podemos recoger automáticamente los headers de la respuesta SOAP.

Sigo sin saber porqué de esta manera se tiene acceso a las cabeceras y con la invocación directa (la forma recomendada) no. Por el momento voy a seguir utilizando el desarrollo que hice en el primer artículo, la invocación directa me parece más elegante y ya que había conseguido recuperar las cabeceras, ¿por qué cambiarlo ahora? 😛 .

Webservices: Tratando con cabeceras SOAP en PHP

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.

Consumiendo webservices SOAP desde PHP

Recientemente nos han llegado un par de proyectos en los que debemos consumir webservices SOAP para obtener datos. La verdad es que hacía bastante tiempo que no los veía en mi trabajo habitual. Hace algunos años eran muy habituales para casi cualquier cosa que implicase comunicación con fuentes externas, sin embargo los últimos años habían caído un poco en desuso para cosas sencillas puesto que complicaban bastante un trabajo que con una simple petición HTTP y un XML básico se podría resolver. Precisamente ésta ha sido siempre una de las mayores críticas al protocolo SOAP, el elevado consumo de ancho de banda para una sencilla petición.

Hasta ahora siempre había utilizado nusoap para realizar llamadas SOAP desde PHP, sin embargo  me encontré con un problema al acceder a un servicio de un importante medio de comunicación internacional. La llamada con nusoap no devolvía resultados mientras que desde el sistema de pruebas html todo funcionaba correctamente.

Después de darle mil vueltas y no encontrar ningún error (otras peticiones al mismo servicio sí que funcionaban) me dí cuenta que PHP ya tiene un conjunto de funciones SOAP nativas, con lo que no necesitaría nusoap. La duda era saber si funcionaría bien, como así fue. Tras añadir la extensión adecuada surge una pequeña incompatibilidad. No puedes usar nusoap y las funcionas nativas en la misma instalación de PHP. Dicho de otro modo, si activas la extensión SOAP, nusoap dejará de funcionar y comenzará a lanzar mensajes de error ya que muchas de las funciones que utiliza tienen el mismo nombre que las nativas, que serían entonces nombres reservados. Si tienes alguna aplicación que utilice nusoap en la misma máquina tendrás que migrarla también para que utilice las funciones nativas.

El ejemplo de hoy será una sencilla llamada a un servicio que nos devuelve un listado de noticias.

El proceso es muy sencillo, necesitas la url del webservice, el método al que vas a llamar y los parámetros a pasarle y, como en cualquier servicio SOAP, te devolverá un XML.

$servicio="http://dominio.com/noticias?wsdl"; //url del servicio
$parametros=array(); //parametros de la llamada
$parametros['idioma']="es";
$parametros['usuario']="manolo";
$parametros['clave']="tuclave";$client = new SoapClient($servicio, $parametros);
$result = $client->getNoticias($parametros);//llamamos al métdo que nos interesa con los parámetros

Con estas sencillas instrucciones ya tenemos en $result el XML resultado de la llamada al servicio. Como trabajar con el XML es un poco engorroso, lo convertimos a un array asociativo de manera que nos sea más sencillo procesar los datos, para ello utilizamos la función obj2array que indico a continuación.

$result = obj2array($result);
$noticias=$result['resultado']['noticias'];
$n=count($noticias);

//procesamos el resultado como con cualquier otro array
for($i=0; $i<$n; $i++){
    $noticia=$noticias[$i];
    $id=$noticia['id'];
    //aquí iría el resto de tu código donde procesas los datos recibidos
}

function obj2array($obj) {
  $out = array();
  foreach ($obj as $key => $val) {
    switch(true) {
        case is_object($val):
         $out[$key] = obj2array($val);
         break;
      case is_array($val):
         $out[$key] = obj2array($val);
         break;
      default:
        $out[$key] = $val;
    }
  }
  return $out;
}

En la segunda línea nos quedamos con los elementos del array que nos interesa procesar. Si no sabes qué devuelve tu webservice puedes hacer un var_dump($result) y verás todo el resultado. En nuestro caso, como es una secuencia de noticias, nos quedamos con el elemento que tiene esas noticias.

Como os habréis dado cuenta, no me he preocupado del control de errores al llamar al webservice.  Eso os lo dejo como ejercicio a vosotros, que es agosto y no me apetece 😛 . En el manual de PHP está toda la información.

Y eso es todo amigos, hoy ha sido un ejemplo sencillo pero muy útil cuando necesitas utilizar SOAP.