11/11/2009

WebServices con NuSOAP en PHP - Ejemplo 3

Este el tercer ejemplo que voy a publicar de la comunicación entre el cliente y servidor de WebServices utilizando NuSOAP con un servidor PHP. Para aquellos que llegarón directamente a esta página, les recomiendo que primero lean el primer ejemplo de NuSOAP con PHP, y luego el segundo ejemplo de NuSOAP con PHP, antes de continuar con este. En este ejemplo en particular voy a modificar el ejemplo 2, para en vez de retornar un objeto, retornar una lista de objetos. En el ejemplo anterior, trate un caso muy particular, que es cuando necesitamos obtener un objeto desde el servidor, en este nuevo ejemplo, voy a obtener un listado de objetos. Muchas veces necesitamos un listado de algún tipo de objeto, por ejemplo, un listado de clientes, con este nuevo Servicio Web planteo este caso, en el cual el servidor nos devuelve un listado con todos los clientes que existen en el sistema.
Como veremos existen algunas modificaciones extras que debemos realizar para retornar un listado con los clientes. Primero debemos definir una estructura que contenga a la definición planteada en el ejemplo anterior, y luego realizar una conversión un poco más compleja (de un vector de objetos, a un vector de vector). También voy a incluír una clase de soporte, que nos ayudará a realizar las conversiones.

Módulo Servidor

Para simplificar la estructura del servidor, intente extraer toda la lógica de conversión en una clase extra (smart_soporte.php), así la lectura del código del servidor se mantiene lo suficientemente limpia como para que sea más fácil su entendimiento. En el ejemplo anterior el servicio web retornaba un cliente al consumidor/cliente de dicho servicio web, en este caso, retornaremos un listado de clientes. Debemos tener en cuenta que las respuestas deben ser convertidas a los tipos de datos básicos, por lo tanto para devolver una lista de clientes, debemos convertir dicha lista en un lista de listas que contengan como indices/keys/claves los atributos de la clase Cliente.
A continuación expongo el código del servidor:
<?php
// Pull in the NuSOAP code
require_once('nusoap/lib/nusoap.php');
require_once('soporte_obrea.php'); // Incluyo este archivo que tiene archivos para soporte de WS

$miURL = 'http://pruebas.orlandobrea.com.ar/nusoap';
$server = new soap_server();
$server->configureWSDL('ws_orlando', $miURL);
$server->wsdl->schemaTargetNamespace=$miURL;



/*
 *  Ejemplo 3: listarClientes es la funcion mas compleja que voy realizar, no recibe parametros
 *  y retorna un listado de clientes. Utiliza varios metodos del ejemplo 2.
 */
 class Cliente {
  var $id;
  var $nombre;
 var $apellido;
 var $cuit;
}
class ClienteDAO {
 /**
  * getCliente($id) : Cliente
  * Esta funcion deberia implementarse con una conexion a una base de datos
  * y obtener la informacion de la base directamente
  */
 function getCliente($id) {
  $obj = new Cliente();
  if ($id==1) {
   $obj->id = 1;
   $obj->nombre = 'Blas';
   $obj->apellido = 'Pascal';
   $obj->cuit = '11-11111111-1';
  }
  if ($id==2) {
   $obj->id = 2;
   $obj->nombre = 'Isaac';
   $obj->apellido = 'Newton';
   $obj->cuit = '22-22222222-2';
  }  
  return $obj;
 }
 
 /**
  * getList : Array
  * Esta funcion retorna un listado de todos los clientes que estan en el sistema.
  * @return 
  */
 function getList() {
  $rta = array();
  $rta[0] = $this->getCliente(1);
  $rta[1] = $this->getCliente(2);
  return $rta;
 }
}

$server->wsdl->addComplexType('Cliente',
 'complexType',
 'struct',
 'all',
 '',
 array(
 'id' => array('name' => 'id', 'type' => 'xsd:int'),
 'nombre' => array('name' => 'nombre', 'type' => 'xsd:string'),
 'apellido' => array('name' => 'apellido', 'type' => 'xsd:string'),
 'cuit' => array('name' => 'CUIT', 'type' => 'xsd:string')
 )
);

$server->wsdl->addComplexType('listadoClientes',
 'complexType',
 'array',
 '',
 '',
 array (array('ref'=>'SOAP-ENC:arrayType','wsdl:arrayType'=>'tns:Cliente[]'))
);
$server->register('listarClientes', // Nombre de la funcion
       array(), // Parametros de entrada
       array('return' => 'tns:listadoClientes'), // Parametros de salida
       $miURL
     );
function listarClientes() {
 $dao = new ClienteDAO();
 $listado = $dao->getList();
 $objSoporte = new SoporteWS();
 $respuesta = $objSoporte->convertirAVectorParaWS($listado);      
 return new soapval('return', 'tns:listadoClientes', $respuesta);
}


$server->service($HTTP_RAW_POST_DATA);
?>
Existen una lista de modificaciones que voy a ir detallando en los siguientes puntos.

Agregar una función en el DAO para que retorne una lista

Como mencione anteriormente en esta oportunidad debemos retornar un listado de clientes, por eso debemos modificar la clase ClienteDAO agregando un nuevo método getList, el cual retorna una lista con todos los clientes.
        function getList() {
  $rta = array();
  $rta[0] = $this->getCliente(1);
  $rta[1] = $this->getCliente(2);
  return $rta;
 }
La clase ObjetoDAO agrega un nuevo método llamado getList(), el cual retorna una lista de todos los clientes que tenemos en el sistema. Al igual que en el ejemplo 2, cuando implementemos este método en un servidor productivo, tendrá una lógica de acceso a datos para obtener la información de alguna fuente de datos. Por ejemplo, si la fuente de datos fuera una base de datos, deberiamos ejecutar una sentencia SQL similar a "select * from Cliente" y luego convertir los resultados en un listado de Clientes.

Definimos la estructura retornada

Debemos definir el nuevo tipo de datos que vamos a retornar, en este caso debemos retornar una lista de clientes, la cual esta definida por la siguiente estructura:
$server->wsdl->addComplexType('listadoClientes',
 'complexType',
 'array',
 '',
 '',
 array (array('ref'=>'SOAP-ENC:arrayType','wsdl:arrayType'=>'tns:Cliente[]'))
);
Como podemos ver en el código anterior, definimos un tipo de datos "listadoClientes" el cual es un array de "Cliente" (el tipo de datos que habíamos definido en el ejemplo 2). Con estas simples lineas estamos indicando que vamos a retornar un listado (vector) y que cada elemento de dicho vector contendrá un Cliente.

Registramos el servicio web a publicar

$server->register('listarClientes', // Nombre de la funcion
       array(), // Parametros de entrada
       array('return' => 'tns:listadoClientes'), // Parametros de salida
       $miURL
     );
Al igual que en los ejemplos anteriores debemos registrar/publiar el servicio web, para ello llamamos a el método register del server. Definimos que el servicio web expuesto llamará a la función "listarClientes" y que la misma retornará un tipo de datos tns:listadoClientes (el tipo de datos definido en el punto anterior).

Implementación de la función

Debemos implementar la función que será llamada cuando el cliente del webservice haga uso del mismo. En este caso será implementado por la función listarClientes().
function listarClientes() {
 $dao = new ClienteDAO();
 $listado = $dao->getList();
 $objSoporte = new SoporteWS();
 $respuesta = $objSoporte->convertirAVectorParaWS($listado);      
 return new soapval('return', 'tns:listadoClientes', $respuesta);
}
Esta función es muy similar a la del ejemplo 2, pero en este caso hacemos uso de un objeto auxiliar (que he creado y el código del mismo esta disponible para descargar al final del artículo) el cual se encarga de la conversión del vector de objetos, en un vector de vector. La conversión es realizada por el método convertirAVectorParaWS del objeto $objSoporte (el cual es de la clase SoporteWS). Al igual que en los ejemplos anteriores, debemos convertir la respuesta en un tipo de datos predefinido, en este caso es el que creamos recientemente (tns:listadoClientes).
Como se puede ver, una vez pasado el ejemplo 1, los pasos son más repetitivos, hemos incorporado algunos objetos de soporte para mantener el código limpio, e intentado hacer el ejemplo cada vez un poco más cercano al mundo real. 

Módulo Cliente

El módulo cliente es el mismo que el del ejemplo 2, simplemente modificamos el nombre del script del servidor y el nombre de la función a llamar. En este caso el script del servidor tiene como nombre "nusoap_server_ej3.php" y la función que vamos a hacer uso en el webservice es "listarClientes"
<?php
require_once('nusoap/lib/nusoap.php');
// Crear un cliente apuntando al script del servidor (Creado con WSDL)
$serverURL = 'http://pruebas.orlandobrea.com.ar';
$serverScript = 'nusoap_server_ej3.php';
$metodoALlamar = 'listarClientes';

$cliente = new nusoap_client("$serverURL/$serverScript?wsdl", 'wsdl');
// Se pudo conectar?
$error = $cliente->getError();
if ($error) {
 echo '<pre style="color: red">' . $error . '</pre>';
 echo '<p style="color:red;'>htmlspecialchars($cliente->getDebug(), ENT_QUOTES).'</p>';
 die();
}


// 2. Llamar a la funcion listadoClientes del servidor
$result = $cliente->call(
    $metodoALlamar,                     // Funcion a llamar
    array(),    // Parametros pasados a la funcion
    "uri:$serverURL/$serverScript",                   // namespace
    "uri:$serverURL/$serverScript/$metodoALlamar"       // SOAPAction
);
// Verificacion que los parametros estan ok, y si lo estan. mostrar rta.
if ($cliente->fault) {
    echo '<b>Error: ';
    print_r($result);
    echo '</b>';
} else {
    $error = $cliente->getError();
    if ($error) {
        echo '<b style="color: red">Error: ' . $error . '</b>';
    } else {
     echo 'Respuesta: ';
  print_r($result);
    }
}
?>

Conclusión

Una vez que vamos avanzando en los ejemplos de WebServices podemos ver la complejidad de los mismos se puede mantener en un nivel similar (ayudandonos de algunos objetos de soporte). Como ya se habrán dado cuenta, no voy a describir la implementación de la clase SoporteWS ya que el código de la misma es código PHP standard, y escapa a los fines del ejemplo. Para simplificar lo que hace esta clase es convertir un objeto en un vector, y un listado de objetos, en un vector de vector. En el próximo ejemplo voy a hacer un ejemplo del mundo real, en el cual se listan los usuarios y al hacer click sobre un enlace, podemos ver todos los datos del mismo. El código de este ejemplo se puede descargar aquí.
Espero que los ejemplos hayan podido introducir a todas aquellas personas que estan realizando webservices, o estan planeando realizar webservices con PHP y NuSOAP.  

AGREGADO: Enlace para descargar el ejemplo completo

12 comentarios:

Anónimo dijo...

Hola, primeramente agredecer los buenos ejemplos de uso de nusoap, muy claro para principiantes como yo, los puse en practica (ejemplo 1 y 2) y funcionan perfectamente, me puse a trabajar con el 3er ejemplo, pero me tropece con el problema conversión del vector de objetos, en un vector de vector, quise descargar el codigo fuente para observar, como estaba realizado pero no hay el enlace :P, gracias nuevamente por la colaboración .

Orlando dijo...

Que bueno que te hayan gustado los ejemplos, y que te haya resultado claro. Y gracias por hacerme dar cuenta que me había olvidado de incluir el enlace para descargar el código, ya esta solucionado. Al pie del post hay un enlace para descargar el ejemplo 3 de NuSOAP con PHP (o lo puedes Descargar de aquí).

Anónimo dijo...

Nuevamente agradecerte, por la respuesta que no se dejo esperar, comentarte que encontre una solución alternativa, claro que mas improvisado y poco trabajo que la clase SoporteWS, igualmente puse a trabajar el codigo fuente que proporcionas y funciona perfectamente, ahora nos vamos al ejemplo para transferir archivos :D

Laureano dijo...

Que tal orlando, eh estado leyendo tus ejemplos de web service, y me parece que te has apuntado un 10 con ellos, felicidades.

Bueno mi duda es que necesito regresar un xml en lugar de un arreglo, me gustaria me ayudaras en como hacerlo, de antemano gracias.


Att: Laureano Naranjo


email. lagos_2_5@hotmail.com
m3trosexual@gmail.com
laureano_naranjo@avansys.com.mx

Orlando dijo...

Muchas gracias Laureano por tus comentarios.

Para retornar un XML, la manera que se me ocurre a priori, es convertir el xml a base64 en el servidor, y luego (des)convertirlo en el cliente.

Espero que me comentes si te resulto de utilidad esta propuesta, sino buscamos otra.

Saludos

Orlando dijo...

Simplemente posteo para informar que hay un nuevo ejemplo, en el cual indica como retornar un XML con NuSOAP. Para acceder al mismo

http://www.orlandobrea.com.ar/2010/05/servicios-web-retornar-un-xml-con.html

Anónimo dijo...

Hola Orlando,

Lo primero agradecerte los buenos ejemplos de NuSoap. Llevo unos cuantos días mirando documentación y no hay nada tan bueno como tus ejemplos.Enhorabuena!! Solo tengo una duda referente a la definición de atributos en un elemento. Yo lo he definido de la siguiente forma, pero no se si es correcto:

$server->wsdl->addComplexType('TipoHabitacion',
'complexType',
'struct',
'all',
'',
array(
'Idioma' => array('name' => 'Idioma', 'type' => 'xsd:string'),
'Codigo' => array('name' => 'Codigo', 'type' => 'tns:AtributoCodigo'),
'Nombre' => array('name' => 'Nombre', 'type' => 'xsd:string')
),
)
);

$server->wsdl->addComplexType('AtributoCodigo',
'attribute',
'struct',
'all',
'',
array(
'Codigo' => array('name' => 'Codigo', 'type' => 'xsd:string')
)
);

Esta es la intención:
< TipoHabitacion Codigo=”DBL”>
< Idioma>ES< /Idioma>
< Nombre> Habitación Doble< /Nombre>
< /TipoHabitacion>

Muchas gracias
Angel

Orlando dijo...

Muchas gracias Angel. Me hace sentir bien que te han resultado de utilidad los ejemplos. A simple vista me parece que esta bien lo que quieres hacer, en cuanto a código.

Es para un caso real lo que quieres utilizar o solo para un ejemplo? esto te lo pregunto porque quizás te sea más engorroso el crear un atributo para mantener solo el código, que definir el mismo en la estructura inicial (tipoHabitacion).

Nuevamente muchas gracias!

Anónimo dijo...

Muchas Gracias Orlando por tu respuesta. Sí, es para un caso real y el problema que la definición de estructura me la obligaba un proveedor, pero al final llegue a un acuerdo con ellos para incluir el código en la estructura inicial, ya que como tú dices, era un engorro. No sé si tiene alguna ventaja indicar dicha información como atributo.

Muchas Gracias por todo!!

Orlando dijo...

Hola Angel, te comento cuales son las ventajas y desventajas de una y otra alternativa:

Ventajas de incluir el atributo en la estructura principal:
+ Más fácil para comenzar
+ Desarrollo más rápido
+ Menos probabilidad de errores

Desventajas:
+ Si la estructura del código cambia mucho en el futuro, el código se volverá un poco más engorroso

Las ventajas y desventajas de "sacar" el atributo actual, a otra estructura (complexType), son las inversas de las de dejarlo. Es decir hoy te complicas un poco más la vida, pero a futuro el código ya te queda más preparado para agregar información extra.

Yo a pesar de todo, iría por la segunda alternativa, dejar el código como un atributo, de esta forma, desarrollo más rápido el día de hoy, y si el día de mañana necesito cambiar la estructura, refactoreo y listo (quizás es un poco más "acorde" a las metodologías ágiles, y no tanto a la de cascada que se intenta dejar de utilizar). Además el código es un campo que parecería que no va a cambiar mucho, ni que incluirá otros atributos.

Éxitos con el proyecto.

Anónimo dijo...

Hola Orlando, soy de nuevo Angel y te escribo para ver si me puedes ayudar con un problema que tengo con mi proyecto. Tengo que definir un método en mi web service que reciba como dato complejo la siguiente estructura
< Estancias>
< Estancia>
< Destino>String
< Noches>Entero
< /Estancia>
< Estancia>
< Destino>String
< Noches>Entero
< /Estancia>
...
< /Estancias>


Y como respuesta el siguiente dato complejo:

< Estancias>
< Destino>
< Codigo>String
< Nombre>String
< /Destino>
< Destino>
< Codigo>String
< Nombre>String
< /Destino>
...
< /Estancias>

Como puedes ver, se repite el dato complejo Estancias, pero tiene distinta estructura, es decir, una es un array de Estancia y la otra es un array de Destino. ¿Cómo puedo declarar este tipo complejo en mi webservice cuando tienen el mismo nombre y distinta estructura? Ya que siempre me coge la ultima definición que indico

Muchas gracias de nuevo.
Un saludo

Orlando dijo...

Hola Angel, antes que nada pido disculpas por la demora, pero no he tenido demasiado tiempo en los ultimos meses.

Lo que debes hacer es definir dos tipos de datos complejos, uno para el parametro que recibe, y otro para el que retorna el webservice (por ejemplo uno se puede llamar estancias y el otro rta_estancias, o como mas te guste).

Saludos