Un día, te llega tu jefe y te dice «Cuando tengas un rato, implementa en PHP una API con SOAP para que se conecten nuestros partners»
o «Para cuando estés aburrido, a ver si te conectas desde nuestro panel PHP a los servicios SOAP de nuestro proveedor».
Lo primero que haces es intentar hacerte una idea desde los fundamentos. Pero cuando comienzas a ojear las especificaciones, te das cuenta que es un bocado difícil de digerir y que el esfuerzo de programar algo desde cero es inviable para la agenda que te han asignado.
Como segunda alternativa, comienzas a googlear a ver si encuentras pequeños tutoriales de personas que hayan tenido que resolver tu misma problemática, a ver si puedes usar sus consejos como plantillas para resolver tu propio problema. Afortunadamente, te encuentras con bastantes ejemplos. A veces son un poco confusos, pero comienzas a hacerte una idea de por dónde puedes encaminarte.
Lo primero, es que decido usar la librería NuSOAP. Lleva tiempo sin actualizarse, pero los ejemplos que encuentro son muy prometedores. Y, poco a poco, a base de prueba y error y leer muchos ejemplos por la red, comienzas a destilar tus propio código adaptado a tus necesidades.
Mi intención en esta entrada no es explicar sistematicamente ni SOAP ni NuSOAP porque tengo todavía muchos cabos sueltos. Pero, al menos, compartir unos cuantos ejemplos simples que quizás ayuden a otros a construir sus propias herramientas. He escrito estos ejemplos a modo de variaciones cada vez más complejas, para que comparándolos podáis entender las diferencias. A ver si estos ejemplos os sirven de ayuda.
Variación número 1: sumar dos números
El primer ejemplo es lo más simple posible (como aperitivo): sumar dos números. Podéis ver el ejemplo operativo aquí.
Primero veamos el código para el servidor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
require_once 'nusoap/nusoap.php'; $service = 'service01'; $server = new soap_server(); $server->configureWSDL($service); $input_array = array('a' => 'xsd:int', 'b' => 'xsd:int'); $return_array = array('result' => 'xsd:int'); $server->register( 'addNumbers', $input_array, $return_array, "urn:$service", "urn:$service#addNumbers", 'rpc', 'encoded', 'Devuelve la suma de los dos numeros que se le pasen como argumento' ); $server->service(file_get_contents('php://input')); function addNumbers($a,$b) { $c = $a + $b; return $c; } |
Ya está. Con este pequeño código, tenemos un servicio SOAP plenamente operativo. NuSOAP hace toda la magia. Básicamente:
- Creas tu objeto
$server
. - Declaras el servicio
addNumbers
que va a tener unos parámetros de entrada y de salida y que asocias a tu función PHPaddNumbers
que defines un poco más abajo. - Ejecutas tu servicio llamando a su método
service
.
Una cualidad de NuSOAP es que no sólo te implementa el servicio, sino que además te construye el documento WSDL necesario para publicarlo. Para este servicio concreto, puedes acceder a su WSDL en esta url. Si lo ojeáis, veréis que es un documento xml donde explica qué es lo que hace el servicio que hemos programado y publicado.
Ahora, veamos el cliente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
require_once 'nusoap/nusoap.php'; class Client01 { const SERVICE_NAME = '/apps/soap/service01.php?wsdl'; const FUNCTION_NAME = 'addNumbers'; private $client; private $log; private $result; public function __construct() { $wsdl = "{$_SERVER['REQUEST_SCHEME']}://{$_SERVER['HTTP_HOST']}" . self::SERVICE_NAME; $this->client = new nusoap_client($wsdl); if (($error = $this->client->getError())) { $this->log("error conectado con el servicio SOAP: $error"); $this->client = null; } } public function getResult() { return $this->result; } public function getLog() { return $this->log; } public function run($firstNumber, $secondNumber) { if ($this->client == null) { return false; } $this->result = null; $this->log = array(); $params=array( 'a' => $firstNumber, 'b' => $secondNumber, ); $this->log('SERVICE ' . self::FUNCTION_NAME . ' REQUEST START'); $result = $this->client->call(self::FUNCTION_NAME, $params); $this->log('SERVICE ' . self::FUNCTION_NAME . ' REQUEST FINISHED'); if (($error = $this->client->getError())) { $this->log('Ha ocurrido algún error'); $this->log($error); return false; } else { $this->log('Todo ok'); $this->result = $result; return true; } } private function log($message) { $this->log[] = $message; } } |
Este código es un poco más extenso. El motivo es que lo he encapsulado en una clase (para facilitar la programación del test que podéis ejecutar) y porque debe tener en cuenta la atención de errores. Vamos a repasar los puntos más importantes:
- En el método
__construct
crea un objetonusoap_client
al que le pasa como parámetro la url correspondiente al documento WSDL publicado por el servicio (por eso, es tan importante el documento wsdl). - El meollo del código está en el método
run
donde declara los parámetros que se le van a pasar al servicio y lo llama mediante el métodocall
. - El resto del código, está destinado a comprobar si ha habido algún error e ir rellenando un array
log
para pasárselo al test de modo que éste pueda tener una traza de lo que ha ocurrido.
Variación número 2: dividir dos números
El primer ejemplo, ha sido lo más simple posible: espera dos enteros como parámetros y devuelve un valor entero como resultado. Pero, normalmente, necesitamos comunicar parámetros más complejos. Por ejemplo, supongamos que quiero programar un divisor y que éste va a devolver una estructura:
- Un
status
que es una cadena que valdrá ‘ok’ si la división se efectuó correctamente o ‘error’ si hubo algún error. - Un
message
con una cadena que es un mensaje referente al resultado. - Un
result
que es el resultado de la división.
Podéis ver el ejemplo operativo aquí (podéis probar a dividir por cero).
Veamos el servicio:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
require_once 'nusoap/nusoap.php'; $service = 'service02'; $server = new soap_server(); $server->configureWSDL($service); $input_array = array('a' => 'xsd:int', 'b' => 'xsd:int'); $return_array = array('status' => 'xsd:string', 'message' => 'xsd:string', 'result' => 'xsd:float'); $server->register( 'divideNumbers', $input_array, $return_array, "urn:$service", "urn:$service#divideNumbers", 'rpc', 'encoded', 'Devuelve la division de los dos numeros que se le pasen como argumento' ); $server->service(file_get_contents('php://input')); function divideNumbers($a,$b) { if ($b == 0) { $status = 'error'; $message = 'no puedes dividir por 0'; $result = null; } else { $status = 'ok'; $message = 'operación procesada correctamente'; $result = $a / $b; } return array('status' => $status, 'message' => $message, 'result' => $result); } |
Si comparáis el servicio anterior con éste, veréis que comparten la misma estructura. Las diferencias son:
- En lugar de devolver un sólo parámetro
result
devuelve tres parámetrosstatus
,message
yresult
. - La función asociada, en lugar de devolver un valor simple, ahora devuelve un array.
En cuanto al cliente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
require_once 'nusoap/nusoap.php'; class Client02 { const SERVICE_NAME = '/apps/soap/service02.php?wsdl'; const FUNCTION_NAME = 'divideNumbers'; private $client; private $log; private $result; public function __construct() { $wsdl = "{$_SERVER['REQUEST_SCHEME']}://{$_SERVER['HTTP_HOST']}" . self::SERVICE_NAME; $this->client = new nusoap_client($wsdl); if (($error = $this->client->getError())) { $this->log("error conectado con el servicio SOAP: $error"); $this->client = null; } } public function getResult() { return $this->result; } public function getLog() { return $this->log; } public function run($firstNumber, $secondNumber) { if ($this->client == null) { return false; } $this->result = null; $this->log = array(); $params=array( 'a' => $firstNumber, 'b' => $secondNumber, ); $this->log('SERVICE ' . self::FUNCTION_NAME . ' REQUEST START'); $result = $this->client->call(self::FUNCTION_NAME, $params); $this->log('SERVICE ' . self::FUNCTION_NAME . ' REQUEST FINISHED'); if (($error = $this->client->getError())) { $this->log('Ha ocurrido algún error'); $this->log($error); return false; } else { $this->log('Todo ok'); $this->result = $result; return true; } } private function log($message) { $this->log[] = $message; } } |
También comparte la misma organización del cliente anterior. Excepto que ahora el resultado $result
que recibe será un array.
Variación número 3: devolver una estructura compleja
Esta tercera variación es muy similar a la anterior: espera un código de empleado y devuelve los datos de dicho empleado. Podéis ejecutar el ejemplo aquí.
Veamos el servidor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
require_once 'nusoap/nusoap.php'; $service = 'service03'; $server = new soap_server(); $server->configureWSDL($service); $server->wsdl->addComplexType('employeeData','complexType','struct','all','', array( 'id' => array('name'=>'id','type'=>'xsd:int'), 'name' => array('name'=>'name','type'=>'xsd:string'), 'department' => array('name'=>'department','type'=>'xsd:string'), ) ); $input_array = array('id' => 'xsd:int'); $return_array = array('status' => 'xsd:string', 'message' => 'xsd:string', 'result' => 'tns:employeeData'); $server->register( 'getEmployeeById', $input_array, $return_array, "urn:$service", "urn:$service#getEmployeeById", 'rpc', 'encoded', 'Devuelve los empleado que cuyo nombre coincida' ); $server->service(file_get_contents('php://input')); function getEmployeeById($id) { $employees = array( array('id' => 1, 'name' => 'Jose Perez', 'department' => 'Ventas'), array('id' => 2, 'name' => 'Juan Ramirez', 'department' => 'Ventas'), array('id' => 3, 'name' => 'Jose Manuel Gonzalez', 'department' => 'Administracion'), array('id' => 4, 'name' => 'Juan Jose Garcia', 'department' => 'Recursos Humanos'), ); $result = null; foreach ($employees as $employee) { if ($employee['id'] == $id) { $result = $employee; } } if ($result !== null) { $status = 'ok'; $message = "empleado id $id encontrado"; } else { $status = 'error'; $message = "empleado id $id no encontrado"; } return array('status' => $status, 'message' => $message, 'result' => $result); } |
Si leéis este nuevo servicio, la diferencia importante es que creamos una estructura employeeData
que será la que se devuelva como resultado ('result' => 'tns:employeeData'
).
Ahora, veamos el cliente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
require_once 'nusoap/nusoap.php'; class Client03 { const SERVICE_NAME = '/apps/soap/service03.php?wsdl'; const FUNCTION_NAME = 'getEmployeeById'; private $client; private $log; private $result; public function __construct() { $wsdl = "{$_SERVER['REQUEST_SCHEME']}://{$_SERVER['HTTP_HOST']}" . self::SERVICE_NAME; $this->client = new nusoap_client($wsdl); if (($error = $this->client->getError())) { $this->log("error conectado con el servicio SOAP: $error"); $this->client = null; } } public function getResult() { return $this->result; } public function getLog() { return $this->log; } public function run($employeeId) { if ($this->client == null) { return false; } $this->result = null; $this->log = array(); $params=array( 'id' => $employeeId, ); $this->log('SERVICE ' . self::FUNCTION_NAME . ' REQUEST START'); $result = $this->client->call(self::FUNCTION_NAME, $params); $this->log('SERVICE ' . self::FUNCTION_NAME . ' REQUEST FINISHED'); if (($error = $this->client->getError())) { $this->log('Ha ocurrido algún error'); $this->log($error); return false; } else { $this->log('Todo ok'); $this->result = $result; return true; } } private function log($message) { $this->log[] = $message; } } |
Esta vez, el cliente no nos aporta nada nuevo. Se limita a recibir el resultado (como un array indexado) y a procesarlo correctamente.
Variación número 4: devolver una lista de empleados
Este último ejemplo, vamos a devolver una lista de empleados. Esperaremos un nombre de empleado y devolverá un array con todos los empleados que tenga dicho nombre. Podéis consultar el ejemplo aquí.
Veamos el servidor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
require_once 'nusoap/nusoap.php'; $service = 'service04'; $server = new soap_server(); $server->configureWSDL($service); $server->wsdl->addComplexType('employeeData','complexType','struct','all','', array( 'id' => array('name'=>'id','type'=>'xsd:int'), 'name' => array('name'=>'name','type'=>'xsd:string'), 'department' => array('name'=>'department','type'=>'xsd:string'), ) ); $server->wsdl->addComplexType('employeeDataArray','complexType','array','','SOAP-ENC:Array', array(), array( array( 'ref' => 'SOAP-ENC:arrayType', 'wsdl:arrayType' => 'tns:employeeData[]', ) ) ); $input_array = array('name' => 'xsd:string'); $return_array = array('status' => 'xsd:string', 'message' => 'xsd:string', 'result' => 'tns:employeeDataArray'); $server->register( 'findEmployeesByName', $input_array, $return_array, "urn:$service", "urn:$service#findEmployeesByName", 'rpc', 'encoded', 'Devuelve los empleado que cuyo nombre coincida' ); $server->service(file_get_contents('php://input')); function findEmployeesByName($name) { $employees = array( array('id' => 1, 'name' => 'Jose Perez', 'department' => 'Ventas'), array('id' => 2, 'name' => 'Juan Ramirez', 'department' => 'Ventas'), array('id' => 3, 'name' => 'Jose Manuel Gonzalez', 'department' => 'Administracion'), array('id' => 4, 'name' => 'Juan Jose Garcia', 'department' => 'Recursos Humanos'), ); $result = array(); $name = strtoupper($name); foreach ($employees as $employee) { if (preg_match("/$name/", strtoupper($employee['name'])) !== 0) { $result[] = $employee; } } $status = 'ok'; $message = count($result) . ' empleados encontrados'; return array('status' => $status, 'message' => $message, 'result' => $result); } |
La clave de este ejemplo es que, una vez definido nuestro tipo complejo employeeData
, definimos un segundo tipo complejo employeeDataArray
que es un array de employeeData
.
El cliente para este servicio es similar al anterior:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
require_once 'nusoap/nusoap.php'; class Client04 { const SERVICE_NAME = '/apps/soap/service04.php?wsdl'; const FUNCTION_NAME = 'findEmployeesByName'; private $client; private $log; private $result; public function __construct() { $wsdl = "{$_SERVER['REQUEST_SCHEME']}://{$_SERVER['HTTP_HOST']}" . self::SERVICE_NAME; $this->client = new nusoap_client($wsdl); if (($error = $this->client->getError())) { $this->log("error conectado con el servicio SOAP: $error"); $this->client = null; } } public function getResult() { return $this->result; } public function getLog() { return $this->log; } public function run($name) { if ($this->client == null) { return false; } $this->result = null; $this->log = array(); $params=array( 'name' => $name, ); $this->log('SERVICE ' . self::FUNCTION_NAME . ' REQUEST START'); $result = $this->client->call(self::FUNCTION_NAME, $params); $this->log('SERVICE ' . self::FUNCTION_NAME . ' REQUEST FINISHED'); if (($error = $this->client->getError())) { $this->log('Ha ocurrido algún error'); $this->log($error); return false; } else { $this->log('Todo ok'); $this->result = $result; return true; } } private function log($message) { $this->log[] = $message; } } |
¿Conclusiones?
Me he quedado con las ganas de comprender al 100% los fundamentos tanto de SOAP como los de NuSOAP. Pero la presión de los proyectos es muy grande. Pero, al menos, conseguí ejecutar la tarea. Espero que estos pequeños ejemplos ayuden a alguien que se vea en la misma encrucijada que yo.