Crear un webservice básico con PHP y SOAP

SOAP es un protocolo de intercambio para servicios web basado en XML, sobre el que puedes leer en esta entrada de la wikipedia. Para no liarnos con preámbulos, vamos con la chicha ¿cómo creamos un servicio web en PHP?.

Para facilitarnos la vida empezaremos por descargar NuSOAP, un toolkit para el desarrollo de servicios web con SOAP en PHP, que nos proveerá de diversas clases para trabajar con este protocolo. Basta con descargarlo, descomprimirlo, meterlo dentro de nuestro proyecto y, cuando queramos usarlo, incluir nusoap.php como librería.

Ok, con NuSOAP instalado toca crear un servidor SOAP en nuestra aplicación. Para el ejemplo crearemos un servidor, lo llamaremos producto.php, que si recibe una petición donde se le pida una lista de libros devuelva tres títulos (es un ejemplo básico, piensa que en la realiad podrías acceder a una base de datos y dar muchas más funcionalidades).

<?php
	require_once "nusoap.php";
	 
        function getProd($categoria) {
	    if ($categoria == "libros") {
	        return join(",", array(
	            "El señor de los anillos",
	            "Los límites de la Fundación",
	            "The Rails Way"));
	    }
	    else {
	            return "No hay productos de esta categoria";
	    }
	}
	 
	$server = new soap_server();
	$server->register("getProd");
	$server->service($HTTP_RAW_POST_DATA);
?>

Ok, ahora necesitas un cliente, que llamaremos cliente.php. En el constructor, el cliente recibirá el url del servidor y para acceder al método que nos devuelve los libros recurriremos al método call(), al cual le pasaremos el nombre del método del servidor al que queremos acceder y los parámetros en forma de array. Además, también controlaremos que no haya errores en la comunicación.

<?php
	require_once "nusoap.php";
	$cliente = new nusoap_client("http://localhost/producto.php");
	 
	$error = $cliente->getError();
	if ($error) {
	    echo "<h2>Constructor error</h2><pre>" . $error . "</pre>";
	}
	 
	$result = $cliente->call("getProd", array("categoria" => "libros"));
	 
	if ($cliente->fault) {
	    echo "<h2>Fault</h2><pre>";
	    print_r($result);
	    echo "</pre>";
	}
	else {
	    $error = $cliente->getError();
	    if ($error) {
	        echo "<h2>Error</h2><pre>" . $error . "</pre>";
	    }
	    else {
	        echo "<h2>Libros</h2><pre>";
	        echo $result;
	        echo "</pre>";
	    }
	}
?>

Con esto ya tienes una idea múy básica del funcionamiento de un webservice SOAP construído con PHP. Pero claro, nos falta un archivo WSDL para tener un webservice decente. Aunque dicho archivo puede ser escrito a mano, NuSOAP puede generarlo por ti pasándole ciertos parámetros, por lo que lo ideal sería generarlo en el servidor. Así que modifica tu producto.php para que quede tal que así:

<?php
	require_once "nusoap.php";
	 
	function getProd($categoria) {
	    if ($categoria == "libros") {
	        return join(",", array(
	            "El señor de los anillos",
	            "Los límites de la Fundación",
	            "The Rails Way"));
	    }
	    else {
	        return "No hay productos de esta categoria";
	    }
	}
	 
	$server = new soap_server();
	$server->configureWSDL("producto", "urn:producto");
	 
	$server->register("getProd",
	    array("categoria" => "xsd:string"),
	    array("return" => "xsd:string"),
	    "urn:producto",
	    "urn:producto#getProd",
	    "rpc",
	    "encoded",
	    "Nos da una lista de productos de cada categoría");
	 
	$server->service($HTTP_RAW_POST_DATA);
?>

Como ves, el cambio en es cuando llamamos a register, ya que en vez de pasarle, como antes, el método en cuestión, le añadimos también varios argumentos para generar el WSDL:

  • El primer array nos permite definir el argumento de entrada y su tipo de datos
  • El segundo define la función de retorno y su tipo de datos
  • urn:producto es la definición del namespace
  • urn:producto#getProd es donde definimos la acción SOAP
  • Luego viene el tipo de llamada,que puede ser rpc, como en el ejemplo, o document
  • Tras esto definimos el valor del atribute use, que puede ser encoded o literal
  • Finalmente viene una descripción de qué hace el método al que llamamos

Ahora basta con que en el navegador accedas a producto.php?wsdl y verás el WSDL generado. Ya puedes copiarlo y añadirlo a tu directorio web (crea un archivo y llámalo, por ejemplo, libros.wsdl). Para que el cliente lo utilice debes modificar el código, y en el constructor, en vez del url le pasas el nombre del archivo, de forma que quede como en el ejemplo:

         $client = new nusoap_client("libros.wsdl", true);

Ahora sí, ya tienes montado un pequeño servicio web. El ejemplo es simplón, pero piensa en todas las funcionalidades que podrías incorporarle.

Convertir una respuesta xml en un objeto de PHP

Por defecto PHP, desde la versión 5.1.2, incorpora la extensión SimpleXML y la trae habilitada (en versiones anteriores es necesario usar –enable-simplexml para activar esta extensión en el momento de compilar). Dicha extensión incluye una serie de herramientas que nos permiten trabajar fácilmente con XML.

No es extraño que si tenemos que comunicarnos con un api externa esta nos devuelva una respuesta en XML, que es un formato universal que puede ser trabajado con casi cualquier lenguaje (otra posibilidad muy habitual es JSON). SimpleXML nos permite recoger esta respuesta, convertirla en un objeto y trabajar con el mismo sin mucha complicación.

En caso de querer recoger una respuesta de otra página, el método a usar sería simplexml_load_string():

$objetoxml = simplexml_load_string($respuestaApi);

Con esto ya tenemos un objeto fácil de recoger. Vamos a imaginar que el objeto que recibimos es tal que así:

<response>
   <requestid>A16</requestid>
   <requests>3</requests>
   <status>error</status>
   <error>
      <code>106</code>
      <message>Request expired</message>
   </error>
</response>

Una notificación de un error conectando con un servicio externo (de hecho, es una copia de un mensaje de error del api de Calameo). Ahora imaginemos que queremos recoger este mensaje de error y mostrárselo por pantalla al usuario. ¿Qué hacemos? Pues muy facilito:

//primero convertimos el xml a objeto
$objetoxml = simplexml_load_string($respuestaApi);

//luego comprobamos que sea un mensaje de error:
if (isset($objetoxml->error))
{
    //e imprimimos
    echo 'Error:'.$objetoxml->error->code.': '.$objetoxml->error->message;
}

Trabajar con el objeto SimpleXML es cosa fácil: Cuando quieres acceder a un elemento basta ir moviéndose como si accedieras a las propiedades o métodos de un objeto. Es decir abuelo->padre->hijo. ¿Que hay varios elementos con la misma etiqueta? Sin problema, las etiquestas se convierten en objetos iterables y accesibles, por lo que puedes recorrerlos recurriendo a los mecanismos básicos de iteración (bucle foreach, por ejemplo).

¿Y para acceder a los atributos? En el ejemplo del xml no hay, pero imaginemos que en lugar del anterior la cosa fuera así:

<response>
   <requestid>A16</requestid>
   <requests>3</requests>
   <status>error</status>
   <errors>
      <error>      
        <message code="106">Request expired</message>
      </error>
      <error>      
        <message code="401">File not found</message>
      </error>
   </error>
</response>

Ok, ahora tendrías que acceder a los atributos, y de paso podríamos ver un ejemplo de iteración. A los atributos se accede como si fuesen elementos de un array, usando como índice su nombre:


//primero convertimos el xml a objeto
$objetoxml = simplexml_load_string($respuestaApi);

//luego comprobamos que sea un mensaje de error:
if (isset($objetoxml->errors))
{
    foreach(objetoxml->errors->error as $error)
    {
        echo 'Error:'.$error['code'].': '.$error->message;
    }
}

Un par de notas más:

  • Para usar el texto de un elemento en una comparación o pasarlo a una función como string hay que forzar la conversión a cadena de texto haciendo un cast con (string)
  • Para acceder a un elemento cuyo nombre viole las convenciones para nombres de objeto de PHP hay que recurrir a encapsular el nombre del elemento dentro de un par de llaves y comillas simples.
  • En este caso hemos hablado de recoger una respuesta de un servicio externo, pero SimpleXML nos permite también recoger un xml desde un archivo con simplexml_load_file().

Y si necesitáis convertir el elemento en un array (en su momento me tocó) ya tratamos ese punto con anterioridad.

Convertir un objeto SimpleXMLElement en un array

Toda la mañana peleando con un API que debería devolverme un array de PHP pero que, cosas de la vida, me devuelve un SimpleXMLElement. Cosillas del PHP y de tener que integrar aplicaciones externas a un proyecto usando sus APIs propias (y, en muchos casos, chapuceras). Tras mucho romperme la cabeza (cosa mala) llegué a la conclusión de que con aquello no había dios que trabajara (o sí, pero necesitaría unos días para aprender a manejarlo) y que mejor me sería pasarlo todo a un array y manipularlo a gusto. Tocó búsqueda por stack overflow y google y aparecieron muchas soluciones.

La mayoría son un truquito tal que así, recorriendo el XML y almacenando sus elementos en un array:

/*Siendo $xml el objeto SimpleXML*/
function xml2array($xml) {
  $arr = array();
  foreach ($xml as $element) {
    $tag = $element->getName();
    $e = get_object_vars($element);
    if (!empty($e)) {
      $arr[$tag] = $element instanceof SimpleXMLElement ? xml2array($element) : $e;
    }
    else {
      $arr[$tag] = trim($element);
    }
  }
  return $arr;
}

Esa es la solución propuesta por php.net, que funciona, sin duda. Pero he visto otra más simple y cortita en Book of Zeus donde lo solucionan con una sola línea:

$xml = json_decode(json_encode((array) simplexml_load_string($string)), 1);

Funciona, lo que no tengo claro es cual cargará menos tu servidor a nivel de rendimiento (eso de codificar como JSON y luego descodificar… mmmmmm, pero a saber). Yo me he decantado por la segunda, que hace el código más legible.

Parsear un XML en Ruby

Ando mirando cosillas de Ruby ya que mañana empiezo un curso de SAAS y entre otras cosas he encontrado cómo parsear un xml tirando de Ruby. En Yahoo respuestas me he encontrado dos opciones: Usar REXML o XML-Simple.

En el caso del primero comentan que es lo más típico, ya que REXML es parte de la librería estandard de Ruby. Y como ejemplo de su uso explican cómo utilizarlo para parsear los datos devueltos por el API de Yahoo Web Search.:

require 'net/http'
require 'rexml/document'

# Búsqueda web de la palabra "madonna"
url = 'http://api.search.yahoo.com/WebSearchService/V1/webSearch?appid=YahooDemo&query=madonna&results=2'

# obtener los datos xml como una cadena
xml_data = Net::HTTP.get_response(URI.parse(url)).body

# extraer la información del evento
doc = REXML::Document.new(xml_data)
titles = []
links = []
doc.elements.each('ResultSet/Result/Title') do |ele|
   titles << ele.text
end
doc.elements.each('ResultSet/Result/Url') do |ele|
   links << ele.text
end

# imprimir todos los eventos
titles.each_with_index do |title, idx|
   print "#{title} => #{links[idx]}\n"
end

En fin, la cosa no parece muy compleja. Para XMLSimple tampoco hay mucha complicación. Se trata de un port de la librería de Perl XMLSimple que os podéis descargar en este enlace y que para muchos es más intuitivo que REXML. En el ejemplo se haría la misma operación que en el anterior, para que compruebes las diferencias en la sintaxis:

require 'net/http'
require 'rubygems'
require 'xmlsimple'

#buscamos "madonna" y lo cogemos como XML
url = 'http://api.search.yahoo.com/WebSearchService/V1/webSearch?appid=YahooDemo&query=madonna&results=2'
xml_data = Net::HTTP.get_response(URI.parse(url)).body

data = XmlSimple.xml_in(xml_data)

#Pateamos los datos y los imprimimos.
data['Result'].each do |item|
   item.sort.each do |k, v|
      if ["Title", "Url"].include? k
         print "#{v[0]}" if k=="Title"
         print " => #{v[0]}\n" if k=="Url"
      end
   end
end

XMLSimple lo que hace es transformar los datos XML a una estructura de datos nativa de Ruby.

En fin, son dos ejemplillos simplones de cómo funcionan ambas librerías para parsear XML. En este enlace tenéis más info sobre REXML y en este sobre XMLSimple para profundizar.

Procedimiento que hace una select desde un XML en SQL-Server

Bueno, estoy realizando un cursillo de SQL-Server2008 R2 cortesía de la Xunta y la UE (hay que aprovechar, que a ver lo que dura la formación gratis en este país). Así que vamos con pequeño ejemplo: un procedmiento que realiza un select sobre un fichero xml, que recibe como parámetro.

Primero vamos a definir una variable como xml (podríamos también importarlo usando un bulk import, pero así veis más clara):

declare @EjemploXML as xml

set @EjemploXML=
'<?xml version="1.0"?>
<SalespersonMods>
    <SalespersonMod SalespersonID="274">
        <Mods>
            <Mod SalesTerritoryID="3"/>
        </Mods>
    </SalespersonMod>
    <SalespersonMod SalespersonID="278">
        <Mods>
            <Mod SalesTerritoryID="4"/>
        </Mods>	
    </SalespersonMod>
<SalespersonMods>'

En fin, ahí tenéis el xml que vamos a recorrer. Como véis la “chicha” está en los atributos SalespersonID y SalesTerritoryID, que son los datos que queremos sacar en la consulta. Ahora os dejo el código y debajo explico todo el procedimiento:


create procedure selectFromXML
@SalesPersonMods xml
as
begin
  declare @XMLdoc int
  exec sp_xml_preparedocument @XMLdoc output, @SalesPersonMods
  select * from
  openxml(@XMLdoc, '/SalespersonMods/SalespersonMod/Mods/Mod')
  with
  (
	SalesTerritoryID int '@SalesTerritoryID',
	SalespersonId int '../../@SalespersonID'
  )

  exec sp_xml_removedocument @XMLdoc
end

En fin, y explicado rápido: Lo primero es crear el procedimiento con create procedure seguido del nombre que queremos darle, y especificando que recibirá como parámetro @SalesPersonMods, que será un xml. Tras eso declaramos un variable entera (la he llamado @XMLdoc) que almacenará un manejador. Para crear dicho manejador usamos el procedimiento almacenado de sistema sp_xml_preparedocument, que recibe como parámetros la variable entera @XMLdoc (definido como output) y la variable xml @SalesPersonMods. Ya tenemos el manejador listo. Ahora queda hacer la select, que empieza como cualquier select normal: select ‘loquesea’ from (en este caso *, o sea, todo). Y en este caso en vez del nombre de tabla irá la función openxml, que recibirá como parámetros el manejador que creamos antes (@XMLdoc) y una cadena de texto con el elemento del xml en el que queremos posicionarnos.

Movernos por el xml es como movernos por MS-DOS, usamos la jerarquía de etiquetas separadas por barras (/) como si fuera una jerarquía de directorios, usando el punto (.) para referirnos a elementos que parten del mismo nivel jerárquico y el doble punto (..) para navegar hacia atrás en la jerarquía. En este caso queremos acceder al atributo SalesTerritoryID, que es un atributo de /Mod (donde nos hemos situado), por lo que no hay que navegar, accedemos a él simplemente con @SalesTerritoryID (a los atributos nos referimos siempre precediéndolos de una arroba, si fuera texto en la etiqueta lo haríamos sin ella) dándole como nombre de columna SalesTerritoryID (definida como entero, en este caso, por ser el valor más adecuado). SalespersonID en cambio está dos niveles más arriba, así que llegamos a él indicándolo con el doble punto: “../../SalespersonID”.

Finalmente, tras todas las operaciones, quitamos el manejador de la memoria para liberarla. Si quieres probar la funcionalidad de esto te basta con copiar y todo el código en el QueryManager de SQL-Server. Finalmente ejecuta el procedimiento pasándole como parámetro la variable sql que definimos arriba y comprueba el resultado.