Archivo de la etiqueta: soap

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.

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.