miércoles, 18 de marzo de 2009

Consumiendo un Servicio Web en PHP desde AJAX, Parte 2: Crear el cliente

Después de una espera un poco larga ya estoy de vuelta para terminar con el tema del servicio Web consumido por AJAX, ahora trabajando del lado del cliente. Antes de comenzar es importante que revisen el tema anterior sobre la creación del servicio Web ya que hice un par de cambios en el código, para agregar un mejor manejo de errores en los métodos (del lado del cliente llamaremos métodos a las funciones PHP del servicio Web). Entonces, ahora nos enfocaremos en el cliente, ¿qué es lo que tendrá que hacer? pues acceder al servicio Web a través de una petición en formato XML, que seguirá el estándar del protocolo SOAP para peticiones, es decir el cliente envía una petición como la siguiente:
<?xml version='1.0' encoding='utf-8'?>
<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'>
<soap:Body>
<buscaAutor xmlns='http://localhost'>
<id xsi:type='xsd:string'>1</id>
</buscaAutor>
</soap:Body>
</soap:Envelope>


En donde la primer etiqueta soap:Envolepe representa el encabezado de una petición SOAP, en el que se establecen los formatos que se utilizarán en la petición, mientras que la etiqueta soap:Body representa el cuerpo de la petición, dentro de la cual se incluyen el nombre del método que se está accediendo (buscaAutor), el namespace del servicio Web (xmlns='http://localhost') y los parámetros que recibirá este método, representando cada parámetro con una etiqueta identificada con el nombre correspondiente. En el ejemplo anterior se envía el parámetro id que es de tipo string y posee el valor de 1.

La respuesta devuelta por el servicio Web tendrá un formato XML que también seguirá el estándar del protocolo SOAP, conteniendo en ella el resultado devuelto por el método al que se accedió, por ejemplo, al acceder al método buscaAutor con un id igual a 1 la respuesta generada es la siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<ns1:buscaAutorResponse xmlns:ns1="http://locahost">
<nombre xsi:type="xsd:string">Pablo Neruda</nombre>
<nacionalidad xsi:type="xsd:string">Chileno</nacionalidad>
<profesion xsi:type="xsd:string">Poeta</profesion>
<error xsi:type="xsd:string"></error>
</ns1:buscaAutorResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>


En donde el servicio Web responde con un archivo XML que inicia con el encabezdo SOAP-ENV:Envelope para indicar los formatos que esta utilizando, seguido por la etiqueta SOAP-ENV:Body dentro de la cual encontramos el cuerpo de la respuesta, que contiene el resultado de la ejecución del método accedido indicada por la etiqueta ns1:buscaAutorResponse, en la que encontramos los valores devueltos por el resultado, cada uno siendo identificado por las etiquetas nombre, nacionalidad, profesión y error, en esta última podemos ver que no se generó algún mensaje de error al encontrarla vacía.

Entonces nuestro cliente AJAX consistirá en un programa que realizará una petición en formato XML, siguiendo las características que indica protocolo SOAP y que además deberá procesar la respuesta también en formato XML para extraer los datos devueltos y desplegarlos en la pantalla.

Programación del cliente

El cliente accederá al servicio Web ServicioDeAutores y podrá consumir cualquiera de sus dos métodos: buscaAutor y actualizaAutor, para lo cual el archivo HTML de la implementación contendrá un formulario que servirá para la doble tarea de capturar y desplegar los datos que serán enviados y recibidos respectivamente.

En esta ocasión volveré a utilizar la biblioteca Prototype que vimos en un post anterior, para facilitar el acceso a los contenidos de la página desde Javascript, sin embargo, no utilizare la funcionalidad para AJAX que ofrece, debido a que esta me arrojo algunos problemas para enviar los parámetros en formato XML, además tampoco me apoyare en otras bibliotecas para Javascript especializadas en el uso de servicios Web, porque no encontré alguna que fuera lo bastante estándar. En cambio, programaré toda la petición y procesamiento de la respuesta utilizando únicamente la clase XMLHttpRequest, dicho de otra forma lo haré todo a mano.

El código completo del archivo HTML para la implementación del cliente es la siguiente:


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Petición a un servicio Web con Ajax, hecha a mano (como los verdaderos hombres)</title>
<script type="text/javascript" src="prototype-1.6.0.3.js"><!-- Referencia a Prototype--></script>
<script type="text/javascript">
var READY_STATE_COMPLETE=4;
var peticionHttp = null;
function inicializaXhr() { //Función que devuelve el objeto de la clase XMLHttpRequest
    if(window.XMLHttpRequest) {
        return new XMLHttpRequest();
    }
    else if(window.ActiveXObject) {
        return new ActiveXObject("Microsoft.XMLHTTP");
    }
}

function peticionDeBusqueda() { //Función en la que se realiza la petición del método buscaAutor en el servicio Web
    var url = "http://localhost/servicio.php"; //URL del servicio Web
    var ns = "http://localhost"; //Namespace del servicio Web
    var arrParam = creaParamBusqueda(); //Obtenemos los parámetros de la busqueda
    var cadenaXML = creaCadenaXMLDePeticion("buscaAutor",ns,arrParam); //Creamos una cadena con formato XML que contendrá la información de la petición    
    peticionHttp = inicializaXhr();
    if(peticionHttp) {
        modificaBoton("btnBuscar","Buscando...",true); //Deahabilitamos el botón de busqueda
        peticionHttp.onreadystatechange = recibeArreglo; //Especificamos la función que obtendrá el arreglo de datos devuelto por el servicio Web
        peticionHttp.open("POST", url, true); //Específicamos el metódo, url y tipo de la petición
        peticionHttp.setRequestHeader ("SOAPAction", "http://localhost/servicio.php/buscaAutor");  //Agregamos al encabezado de la petición el indicador del protocolo SOAP
        peticionHttp.setRequestHeader ("Content-Type", "text/xml");  //Agregamos al encabezado de la petición que el contenido se trata de un documento XML
        peticionHttp.send(cadenaXML); //Enviamos los parámetros
    }
    $('divRespuesta').innerHTML = ""; //*Nota: El símbolo $ es equivalente a document.getElementById
}

function peticionDeActualizacion() { //Función en la que se realiza la petición del método actualizaAutor en el servicio Web
    var url = "http://localhost/servicio.php"; //URL del servicio Web
    var ns = "http://localhost"; //Namespace del servicio Web
    var arrParam = creaParamEnvio(); //Obtenemos los parámetros del envio de datos para la actualización
    var cadenaXML = creaCadenaXMLDePeticion("actualizaAutor",ns,arrParam); //Creamos el objeto XML que contendrá la información de la petición    
    peticionHttp = inicializaXhr();
    if(peticionHttp) {
        modificaBoton("btnGuardar","Guardando...",true);
        peticionHttp.onreadystatechange = recibeMensaje;
        peticionHttp.open("POST", url, true);
        peticionHttp.setRequestHeader ("SOAPAction", "http://localhost/servicio.php/actualizaAutor");
        peticionHttp.setRequestHeader ("Content-Type", "text/xml");
        peticionHttp.send(cadenaXML);
    }
}

function recibeArreglo() { //Función que manejara la respuesta recibida por el servidor al solicitar el método buscaAutor
    if(peticionHttp.readyState == READY_STATE_COMPLETE) {
        if(peticionHttp.status == 200) {
            var documentoXML = peticionHttp.responseXML;            
            var objResp = {nombre: "", nacionalidad: "", profesion:""}; //Las propiedades identifican el nombre del campo que deseamos extraer del documento
            procesaXML(documentoXML, "buscaAutor", objResp);
            if(!objResp.error) //Verificamos si se obtuvo algún error de la respuesta
            {
                $('txtNombre').value = objResp.nombre;
                $('txtNacionalidad').value = objResp.nacionalidad;
                $('txtProfesion').value = objResp.profesion;
            }
            else
                alert(objResp.error);
            modificaBoton("btnBuscar","Buscar",false);
        }
    }
}

function recibeMensaje() {//Función que manejara la respuesta recibida por el servidor al solicitar el método buscaAutor
    if(peticionHttp.readyState == READY_STATE_COMPLETE) {
        if(peticionHttp.status == 200) {
            var documentoXML = peticionHttp.responseXML;
            var objResp = {mensaje: ""}; //Las propiedades identifican el nombre del campo que deseamos extraer del documento
            procesaXML(documentoXML, "actualizaAutor", objResp);
            if(objResp.mensaje.indexOf("Error") < 0) //Verificamos si la cadena contiene algún error
                $('divRespuesta').innerHTML = objResp.mensaje;
            else
                alert(objResp.mensaje);
            modificaBoton("btnGuardar","Guardar",false);
        }
    }
}

function procesaXML(documXML, nomMetodo, objSalida) { //Función que extre del objeto ducumXML los valores devueltos por el Servicio Web dentro del objeto objSalida
    var nomRespuesta = "ns1:" + nomMetodo + "Response";
    var raiz = documXML.getElementsByTagName('SOAP-ENV:Envelope')[0];
    var cuerpo = raiz.getElementsByTagName('SOAP-ENV:Body')[0];
    if(!cuerpo.getElementsByTagName('SOAP-ENV:Fault')[0])
    {   
        var respuesta = cuerpo.getElementsByTagName(nomRespuesta)[0];
        for(var campo in objSalida)
        {
            if(respuesta.getElementsByTagName(campo)[0].firstChild)
                objSalida[campo] = respuesta.getElementsByTagName(campo)[0].firstChild.nodeValue;
        }
    if(respuesta.getElementsByTagName('error')[0] && respuesta.getElementsByTagName('error')[0].firstChild)
        objSalida.error = respuesta.getElementsByTagName('error')[0].firstChild.nodeValue;
    }
    else {
        var respuesta = cuerpo.getElementsByTagName('SOAP-ENV:Fault')[0];
        objSalida.error = respuesta.getElementsByTagName('faultstring')[0].firstChild.nodeValue;
    }
}

function creaCadenaXMLDePeticion(nomMetodo, ns, arrParametros) { //Devuelve una cadena que representa al documento XML que será enviado como petición al Servicio Web
    var cadenaXML = "<?xml version='1.0' encoding='utf-8'?>";
    cadenaXML += "<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' ";
    cadenaXML += "xmlns:xsd='http://www.w3.org/2001/XMLSchema' ";
    cadenaXML += "xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'>";
    cadenaXML += "<soap:Body>"
    cadenaXML += "<" + nomMetodo + " xmlns='"+ ns +"'>"; //Elemento XML en el que aparece el nombre del método solicitado y el namespace especificado
    tope = arrParametros.length;
    for(var i=0; i < tope; i++) { //Para cada parámetro que requiere el método se crea un elemento con la información del objeto del arreglo correspondiente
        cadenaXML += "<"+arrParametros[i].nombre+" xsi:type='xsd:"+arrParametros[i].tipo+"'>"+arrParametros[i].valor+"</"+arrParametros[i].nombre+">";
    }
    cadenaXML += "</"+nomMetodo+">";
    cadenaXML += "</soap:Body></soap:Envelope>";
    return cadenaXML;
}

function creaParamBusqueda() { //Función que crea un arreglo que contiene el objeto del parámetro para la busqueda
    var arreglo = new Array(
    {nombre:'id', valor: $("txtId").value, tipo: "string"}
    ); //El objeto representa el parametro id de la función buscaAutor
    return arreglo; //*Nota: El símbolo $ es equivalente a document.getElementById, esta es una funcionalidad que nos ofrece la biblioteca Prototype
}

function creaParamEnvio() {//Función que crea un arreglo que contiene los objetos de los parámetros para la actualización
    var arreglo = new Array(
    {nombre:'id', valor: $("txtId").value, tipo: "string"},
    {nombre:'nombre', valor: $("txtNombre").value, tipo: "string"},
    {nombre:'nacionalidad', valor: $("txtNacionalidad").value, tipo: "string"},
    {nombre:'profesion', valor: $("txtProfesion").value, tipo: "string"}
    ); //Cada uno de los objetos que se encuentran en el arreglo representan los parámetros de la función del servicio Web (id, nombre, nacionalidad, profesion)
    return arreglo; //*Nota: El símbolo $ es equivalente a document.getElementById, esta es una funcionalidad que nos ofrece la biblioteca Prototype
}

function modificaBoton(id, valor, deshabilitar) { //Función que puede habilitar o deshabilitar de un elemento especificado, así como cambiar su valor
    if($(id)) {
        if(valor !="")
            $(id).value = valor;
        $(id).disabled = deshabilitar;        
    }
}
</script>
</head>
<body>
<form>
<p><label>Id:</label> <input type="text" name="txtId" id="txtId" size="5" /> <input type="button" value="Buscar" id="btnBuscar" onclick="peticionDeBusqueda()" /></p>
<p>
  <label>Nombre:</label>
  <input type="text" id="txtNombre" name="txtNombre" size="40" /><br/>
  <label>Nacionalidad:</label>
  <input type="text" id="txtNacionalidad" name="txtNacionalidad" size="30" /><br/>
  <label>Profesion:</label>
  <input type="text" id="txtProfesion" name="txtProfesion" size="50" /><br/>
  <input type="button" value="Guardar" id="btnGuardar" onclick="peticionDeActualizacion()" /></p>
  </form>
<div id="divRespuesta"></div>
</body>
</html>


Como pueden ver, es un código algo largo y es que incluí algunas funcionalidades adicionales a lo que habíamos venido manejando hasta ahora. Una vez más recurrimos a la función inicializaXhr() para acceder a la instancia de la clase XMLHttpRequest, y este objeto es utilizado para iniciar dos peticiones dentro de las funciones peticionDeBusqueda y peticionDeActualizacion.

function peticionDeBusqueda() {
  var url = "http://localhost/servicio.php";
  var ns = "http://localhost";
  var arrParam = creaParamBusqueda();
  var cadenaXML = creaCadenaXMLDePeticion("buscaAutor",ns,arrParam);
  peticionHttp = inicializaXhr();
  if(peticionHttp) {
...


La función peticionDeBusqueda() será llamada al hacer clic sobre el botón btnBuscar, y al hacerlo se iniciará la petición a nuestro servicio Web del método buscaAutor. Como vemos en el código se define la URL del serivico Web, se declara el namespace de este servicio (ns) y después por medio de la función creaParamBusqueda, accedemos al valor del campo id capturado por el usuario en el formulario, el cual es alamacenado en un arreglo de objetos llamado arrParam, este arreglo es pasado como parámetro a la función creaCadenaXMLPeticion(), la cual utiliza éste y otros dos parámetros para crear una cadena con formato XML que representará el mensaje XML de la petición del método buscaAutor (siguiendo el formato que definimos al inicio de este post). En seguida se crea la instancia de la clase XMLHttpRequest y después de validarla se procede a realizar la petición:

modificaBoton("btnBuscar","Buscando...",true);
peticionHttp.onreadystatechange = recibeArreglo;
peticionHttp.open("POST", url, true);
peticionHttp.setRequestHeader ("SOAPAction", "http://localhost/servicio.php/buscaAutor");
peticionHttp.setRequestHeader ("Content-Type", "text/xml")
peticionHttp.send(cadenaXML);


Como vemos, se realiza una llamada a la función modificaBoton, cuya única función es deshabilitar el botón del formulario btnBuscar, para evitar que se pulse otra vez mientras se atiende la petición del servicio Web. En seguida se hace referencia a la función recibeArreglo() para indicar que se ejecute en cuanto se complete la solicitud al servidor Web, después con el método open se establecen las características de la petición al servidor Web. El método setRequestHeader se utiliza para agregar dos encabezados adicionales dentro de la petición, el primero para indicar que se trata de una petición que implementa el protocolo SOAP por medio del identificador "SOAPAction", y el segundo para indicar que el contenido de la petición tendrá un formato XML por medio del identificador "Content-Type" de tipo "text/xml". De esta forma, el texto que contiene cadenaXML será interpretado como el contenido XML de la petición enviada al servidor Web al momento de pasárselo a la al métod send, con lo cual se inicia la petición del método buscaAutor al servicio Web ServicioDeAutores.

La función peticionDeActualizacion() se iniciará haciendo clic sobre el botón identificado como btnGuardar, y su funcionamiento es muy parecido al de peticionDeBusqueda, con la diferencia de que aquí el método que se solicita es actualizaAutor y al cual se le proporcionan los datos del autor capturados en el formulario, a los cuales se accede por medio de la función creaParamEnvio():
var arreglo = new Array(
    {nombre:'id', valor: $("txtId").value, tipo: "string"},
    {nombre:'nombre', valor: $("txtNombre").value, tipo: "string"},
    {nombre:'nacionalidad', valor: $("txtNacionalidad").value, tipo: "string"},
    {nombre:'profesion', valor: $("txtProfesion").value, tipo: "string"}
    );
    return arreglo;


Que como vemos simplemente crea un arreglo que contiene los valores capturados en este formulario, dentro de una serie de objetos (delimitados con la apertura y cierre de llaves "{}"), en los que además del valor se establece el nombre y tipo de dato para los valores recogidos, debido a que este arreglo se utilizará en la función creaCadenaXMLDePeticion para elaborar el texto del contenido XML para la petición, en donde los valores de este arreglo se usarán para establecer los parámetros del método que se solicita:

var cadenaXML = "<?xml version='1.0' encoding='utf-8'?>";
cadenaXML += "<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' ";
cadenaXML += "xmlns:xsd='http://www.w3.org/2001/XMLSchema' ";
cadenaXML += "xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'>";
cadenaXML += "<soap:Body>"
cadenaXML += "<" + nomMetodo + " xmlns='"+ ns +"'>";
tope = arrParametros.length;
for(var i=0; i < tope; i++) {
    cadenaXML += "<"+arrParametros[i].nombre+" xsi:type='xsd:"+arrParametros[i].tipo+"'>"+arrParametros[i].valor+"</"+arrParametros[i].nombre+">";
}
cadenaXML += "</"+nomMetodo+">";
cadenaXML += "</soap:Body></soap:Envelope>";
return cadenaXML;


En la función creaCadenaXMLDePeticion la variable cadenaXML es utilizada para guardar el cuerpo de la petición XML que se va generando dentro, utilizando el nombre del método, nombre de namespace y el arreglo de valores que recibe como parámetros.

Una vez que se recibe la respuesta del servidor Web, en el caso del método actualizaAutor se ejecuta la función recibeMensaje, la cual después de validar el estado de la respuesta se encarga de procesar su contenido:
var documentoXML = peticionHttp.responseXML;
var objResp = {mensaje: ""};
procesaXML(documentoXML, "actualizaAutor", objResp);
if(objResp.mensaje.indexOf("Error") < 0)
    $('divRespuesta').innerHTML = objResp.mensaje;
else
    alert(objResp.mensaje);
modificaBoton("btnGuardar","Guardar",false);


Creando un objeto XML llamado documentoXML que representará el documento XML devuelto por el servicio Web, y podremos utilizarlo para extraer de él la información que nos interesa. En este caso queremos conocer el contenido del campo mensaje que el servicio devuelve como resultado de la ejecución del método actualizaAutor. Para lograrlo utilizamos la función procesaXML que recorre todo el documento XML y almacena dentro de una propiedad del objeto objResp el valor del campo que necesitamos. Una vez que tenemos a la mano este valor, podemos desplegar el mensaje devuelto por el servicio con la instrucción:
$('divRespuesta').innerHTML = objResp.mensaje;


Donde el simbolo '$' equivale a document.getElementById, la cual es una funcionalidad que nos ofrece Prototype para hacer nuestra captura más rápida.

Almacenando este archivo HTML podemos pasar a probar su funcionamiento, teniendo presenta cual es la URL del servicio Web que accederemos, por si se necesitan hacer cambios las url contenidas en el código Javascript.

Nota: Una vez más les recuerdo que antes de probar el servicio Web modifiquen el código PHP agregando la contraseña de acceso a su SMBD MySQL en la parte indicada por [TuContraseña].

Al acceder al cliente por medio de nuestro navegador Web, este debe tener una apariencia como la que sigue:


Podemos probar el servicio de búsqueda de autores capturando un identificador como sigue:


Si capturamos un identificador que no exista en la tabla de la base de datos el sistema nos responde los siguiente:


Si capturamos cambios en la información de un autor consultado, podemos probar el método para la actualización de la información:


O, si capturamos un nuevo identificador y hacemos clic en guardar el sistema interpretará que se trata de un nuevo registro realizando una inserción en la tabla:


Y de esta forma es que este pequeño sistema funciona, es obvio que le hacen falta diferentes validaciones, pero cumple con el propósito que nos fijamos al inicio al consumir exitosamente los métodos del servicio Web desarrollado.

Conclusiones

Este fue el último post dedicado al aprendizaje de AJAX dentro de este blog, con todas las anotaciones hechas hasta aquí considero que cubrí todas las expectativas que tenía al iniciar comenzar a aprender sobre este componente de Javascript, lo cual no significa que ya no dedicaré su tiempo a los temas relacionados con AJAX, sino que ya no me centraré en asuntos de nivel básico al momento de referirme estos, y que ahora exploraré otras herramientas de software para seguir haciendo pequeñas guías como esta. Si algún cibernauta se topa con este blog buscando un guía sobre el lenguaje AJAX espero que todos estos artículos le sean de utilidad, para cualquier duda o critica que quieran hacer esta la opción para dejar sus comentarios, los cuales siempre se agradecen.

Hasta la próxima.

0 comentarios:

Publicar un comentario