5.2. Web Services

5.2.1. Web Services en PHP

5.2.1.1. Cliente

La creación de un cliente de un servicio web con PHP es relativamente sencilla haciendo uso de PHP-SOAP. Con la descripción del servicio al que queremos acceder (fichero wsdl), obtendremos acceso a todos los métodos que ofrece el servicio web. A continuación mostramos un ejemplo donde se verá más claramente lo expuesto. Concretamente en el ejemplo llamamos a un WS que, dada una cadena, devuelve la cadena al revés.

$objClienteWS = new SoapClient('Ws_Ejemplo.wsdl');
$resultado = $objClienteWS->ejemplo('Hola');
print_r($resultado);

De la ejecución de este cliente obtenemos el siguiente resultado:

  aloH

5.2.1.2. Servidor

La creación del servidor requiere, evidentemente, algo más de trabajo que la del cliente. En este punto haremos un pequeño resumen de los pasos a seguir. Primero tenemos que crear un fichero php (en nuestro ejemplo server.php) que contendrá las llamadas a las clases SOAP correspondientes al servidor. En este mismo fichero se puede incluir la definición de la clase que implementará todos los métodos exportados. Siguiendo con nuestro ejemplo, tenemos que tener un método que nos devuelva la inversa de una cadena. El contenido del fichero es:

require_once 'SOAP/Server.php';
  
 class Prueba_Server {          
    function ejemplo($cadena){
      return strrev($cadena);
    }    
 }

 $server = new SOAP_Server;
 $server->_auto_translation = true;
 $soapclass = new Prueba_Server();
 $server->addObjectMap($soapclass,'urn:Prueba_Server');
 $server->service($HTTP_RAW_POST_DATA);

Para que el cliente tenga acceso a la información que ofrece el WS, necesita de la definición de los métodos exportados por la clase. Esto se obtiene a partir del fichero WSDL. El fichero de nuestro ejemplo es el siguiente:

<?xml version="1.0"?>
<definitions name="ServerExample" targetNamespace="urn:ServerExample"
  xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
  xmlns:tns="urn:ServerExample"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
  xmlns="http://schemas.xmlsoap.org/wsdl/">

<types xmlns="http://schemas.xmlsoap.org/wsdl/"></types>
<message name="ejemploRequest">
  <part name="cadena" type="xsd:string" />
</message>
<message name="ejemploResponse">
  <part name="cadena" type="xsd:string" />
</message>
<portType name="ServerExamplePort">
  <operation name="ejemplo">
    <input message="tns:ejemploRequest" />
    <output message="tns:ejemploResponse" />
  </operation>
</portType>
<binding name="ServerExampleBinding" type="tns:ServerExamplePort">
 <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" />
  <operation name="ejemplo">
    <soap:operation soapAction="urn:Prueba_Server#prueba_server#ejemplo" />
     <input>
       <soap:body use="encoded" namespace="urn:Prueba_Server" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
     </input>
     <output>
       <soap:body use="encoded" namespace="urn:Prueba_Server" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
     </output>
  </operation>
</binding>
<service name="ServerExampleService">
 <documentation />
  <port name="ServerExamplePort" binding="tns:ServerExampleBinding">
   <soap:address location="http://mi-servidor.es/cin/ws/server/server.php" />
  </port>
</service>
</definitions>

Con esto nuestro Web Service ya esta funcionando. Simplemente tenemos que llamarlo desde el cliente.

5.2.2. Generación del WSDL

Uno de los puntos más costosos de la creación de un WS en PHP es la generación del WSDL. Hay una serie de herramientas que te permiten su generación siempre y cuando rellenes ciertos parámetros. El propio PEAR::SOAP te facilita un proceso para hacerlo, y es el que vamos a intentar explicar a continuación.

La idea consiste en crear un constructor para nuestra clase y, en él, sobrescribir una serie de arrays donde aportaremos la información de los métodos que exporta una clase. Concretamente se trata de la estructura $this->__dispatch_map que tendrá una entrada por cada uno de los métodos soportados. Después, tras las llamadas que realizamos al SOAP_Server tenemos que utilizar una clase de SOAP que nos generará el WSDL.

El código para nuestro ejemplo es el siguiente:

 require_once 'SOAP/Server.php';
  
 class Prueba_Server {          
    function Prueba_Server(){
        $this->__dispatch_map['ejemplo'] =
            array(
                'in' => array('cadena' => 'string',
                        ),
                'out' => array('cadena' => 'string'),
            );
    }
    function ejemplo($cadena){
      return strrev($cadena);
    }    
 }

 
 $server = new SOAP_Server;
 $server->_auto_translation = true;
 $soapclass = new Prueba_Server();
 $server->addObjectMap($soapclass,'urn:Prueba_Server');
 
 if(isset($_REQUEST['wsdl'])){ 
   require_once 'SOAP/Disco.php';
   $disco = new SOAP_DISCO_Server($server,'ServerExample');
   header("Content-type: text/xml");
   echo $disco->getWSDL();
   return;
 }
 
 $server->service($HTTP_RAW_POST_DATA);

Con este código, al interrogar al server.php directamente, obtendremos un xml que contiene la definición del WS. Este contenido se almacena en un fichero WSDL y, de ese modo, los clientes podrán acceder al servicio.

5.2.3. Web Services en gvHIDRA

Ya hemos visto como se implementan los WS en PHP. Para aplicaciones con gvHidra, se han implementado algunas clases que ayudan a la creación tanto del servidor como del cliente en una aplicación. De momento se utiliza un procedimiento muy básico para controlar la seguridad.

5.2.3.1. Cliente

La creación del cliente se realiza mediante la clase IgepWS_Client. A continuación mostramos el código:

$objCIN = IgepWS_Client::getClient('actions/ws/WSCIN.wsdl');
$credencial = IgepWS_Client::getCredential('WSCIN');
$objCIN->getImporte($credencial,....

El parámetro credencial contiene los parámetros de validación válidos para ese cliente WS. Este parámetro se obtiene consultando los DSNs de la aplicación. La estructura de la credencial seria la siguiente:

$credencial = array(
               'login'=>'XXXXX',
               'password'=>'XXXXX'
   );

Esta clase usa SoapClient, y si necesitamos pasarle otras opciones podemos hacerlo con el parámetro opcional. En el ejemplo siguiente se modifican las opciones para poder depurar la comunicación con el servidor. En la web de PHP se pueden ver otras opciones posibles.

$obj = IgepWS_Client::getClient('actions/ws/WSCIN.wsdl', array("exceptions" => 0, 'trace'=>1, ));
print "<pre>\n";
print "Request :       \n". htmlspecialchars($obj->__getLastRequest())  ."\n";
print "RequestHeaders :\n". $obj->__getLastRequestHeaders()             ."\n";
print "Response:       \n". htmlspecialchars($obj->__getLastResponse()) ."\n";
print "ResponseHeaders:\n". $obj->__getLastResponseHeaders()            ."\n";
print "Functions:      \n". var_export($obj->__getFunctions(),true)     ."\n";
print "Types:          \n". var_export($obj->__getTypes(),true)         ."\n";
print "</pre>";

Para depuración, también puede ser útil inhabilitar la cache del cliente, ya que así podemos ir modificando el wsdl:

IgepWS_Client::disableCache();

Si el cliente usa una codificación distinta a UTF-8 (como ocurre con gvHIDRA que usa latin1), conviene indicarlo en las opciones de conexión y así no hay que hacer conversiones explicitas, sino que PHP convierte los parámetros de entrada desde la codificación origen a UTF-8, y el resultado lo convierte de UTF-8 a la codificación indicada. Si no lo indicamos se fija a 'latin1'.

Ejemplo:

$obj = IgepWS_Client::getClient('actions/ws/WSCIN.wsdl', array("exceptions" => 0, 'trace'=>1, ));
...
print "Response:\n".htmlspecialchars(utf8_decode($obj->__getLastResponse()))."\n";
...

5.2.3.2. Servidor

En el caso del servidor, los beneficios son mayores. Tenemos dos clases, una clase estática que genera el código básico del Servidor y otra que proporciona un comportamiento al servidor propio de una aplicación gvHidra. La primera de ellas es la clase estática IgepWS_ServerBase. Esta clase simplifica la creación y el registro de un servidor SOAP de WS. Concretamente, en nuestro fichero de lanzamiento del servidor (típicamente server.php), que tiene que estar en el raiz de la aplicación, tendríamos el siguiente código:

 require_once "igep/include/igep_ws/IgepWS_ServerBase.php";
 require_once 'ws/server/WSCIN.php';
 
 IgepWS_ServerBase::registrar('WSCIN');

Por otro lado, tenemos que crear la clase que controla el Servidor (en nuestro ejemplo WSCIN). Esta clase, al heredar de IgepWS_Server tiene el mecanismo de validación ya implementado y el sistema de conexión propio de gvHidra. La única premisa que se exige es que, si se requiere validación, el método implementado por el programador debe incluir un parámetro $credencial que se pasará al método checkCredential para validar su contenido.

A continuación mostramos un ejemplo:

include_once "igep/include/igep_ws/IgepWS_Server.php";
  
class WSCIN extends IgepWS_Server
{
    function __construct()
    {
        $msgs = array(
                '1'=>'Error de conexión. Avise al Servicio de Informática',
                '2'=>'Error en operación. Avise al Servicio de Informática',
        );
        parent::IgepWS_Server('WSCIN', $msgs);
        ...
    }
        
    function getImporte($credencial,$anyox, $dgralx, $numx, $tipo_expx, $numtipo_expx)
    {
      if (!$this->checkCredential($credencial))
        return $this->getAuthError();
      $dsn = ConfigFramework::getConfig()->getDSN('dsn_cin');
      $db = $this->conectar($dsn);
      if (!$db)
        return $this->gvhFault('1');
      ...
      return array('implic' => floatval($res[0]['implic']), 'impadj' => floatval($res[0]['impadj']));
    }
    ...
}

Si nuestro servidor acepta varias credenciales, podemos pasarle un vector al constructor del padre:

parent::IgepWS_Server(array('WSCIN','WSMCMENOR',), $msgs);

En las credenciales de los servidores de web services, la contraseña hay que almacenarla con hash. Para ello usar el formulario en igep/include/igep_utils/protectdata.php para obtener los hash de las contraseñas, y guardar estas últimas en un lugar seguro, fuera de la aplicación.

El parámetro con los mensajes de error se utiliza si provocamos los errores (Soap_Fault) con el método gvhFault, que hace las conversiones necesarias en la codificación y tiene un parámetro opcional usado para que nos informe del error en el debug (tener en cuenta que los errores de conexión y los del método consultar ya se registran en el debug). Por ejemplo:

return $this->gvhFault('2','Error consultando tabla');

Si queremos restringir algún método a algunas credenciales, podemos hacerlo con método checkCredential pasándole la lista de credenciales:

if (!$this->checkCredential($credencial, array('WSMCMENOR',)))
        return $this->getAuthError();

También hay que tener precaución cuando hacemos consultas sobre base de datos, que si utilizamos campos calculados o agregados (count, min, ...), el resultado será de tipo string. Si queremos obtener otro tipo tendremos que modificarlo usando la función floatval para tipo float, intval para tipo int, ... En caso de problemas podemos forzar las conversiones con usando la clase SOAP_Value donde básicamente le indicamos el nombre del atributo, el tipo y el valor.

Ejemplo:

$modulos_usuario = new SOAP_Value('modulos', 'ModulosValidaStruct', $array_con_modulos);

También es importante tener en cuenta la codificación, sobretodo cuando nuestro WS recibe o devuelve campos string que puedan tener carácteres especiales.

Cuestiones:

  • la codificación usada en los WS es UTF-8, luego habrá que hacer las transformaciones necesarias desde/hacia LATIN-1 (la codificación usada en gvHIDRA)

  • si queremos retornar una cadena obtenida de la BD o de una constante en un fichero fuente de PHP, tenemos que transformarla a utf-8 con utf8_encode($cadena), o mejor usamos el método encode.

  • si recibimos un parámetro texto vendrá en utf-8, luego también habrá que transformarlo (utf8_decode) a latin-1 para operar con él (concatenar con otras cadenas, almacenar en BD, ...). Lo podemos hacer con el método decode.

  • en caso de problemas también podemos hacer las transformaciones con iconv.

En caso de problemas con algún tipo de datos conviene consultar la documentación en http://www.w3.org/TR/xmlschema-2.