#SOA #REST sobre #cakePHP, consumido por #jQuery

Publicado por Norberto Herz el

Post cargadito. Recomendación del autor: Cafecito y media horita para leer en detalle.

Introducción

Lo más dificil de este POST fue escribir un título que refleje el contenido. Creo que no lo logré, y por eso estas primeras líneas intentaran explicar el por qué de la mezcla de temas.
Conociendo los conceptos básicos de REST, y necesitando desarrollar una aplicación por mi cuenta (es decir, sin restricciones corporativas de ningún tipo), decidí profundizar en estos conceptos y llevarlos a la práctica. Cuando quiero desarrollar un proyecto rápido, sin necesidades particulares de performance y escalabilidad, elijo PHP por la simpleza del start up principalmente (luego, si el proyecto crece, evalúo migrar de tecnología). Habiéndo trabajado previamente con cakePHP (de una manera muy básica) decidí profundizar también en su utilización como MVC, no sin antes investigar acerca de su soporte a REST. Consumir los servicios generados mediante jQuery, persigue el objetivo de lograr una RIA (Rich Internet Application) y la elección del framework responde solamente al gusto y la necesidad también de profundizar mis conocimientos sobre el mismo (otros proyectos me han llevado a conocer extJS y DOJO) y quería extenderme en jQuery y jQuery UI.
En definitiva (y también a modo de advertencia) lo único que tienen en común estos tópicos, es que no soy experto en ninguno de ellos, lo cual podría llevar a una pregunta final: ¿Por qué escribir sobre algo que hasta hace poco tiempo fue desconocido, y en lo que claramente no soy experto? Simple:
1. Me costó bastante trabajo encontrar buena documentación acerca de REST sobre cakePHP. La documentación oficial está sencillamente incompleta, y las mismas preguntas que me surgieron a mi, le surgieron a muchas personas, pero las respuestas no se encontraban fácilmente.
2. REST en si mismo, se basa en la simpleza de reutilizar un protocolo existente, evitando sumar una complejidad que desde su punto de vista se considera innecesaria (haciendo referencia al típico esquema SOAP y los WSDL). Dicho esto, creo que sería un contrasentido buscar un experto REST. Tal cosa no debería existir.
3. ¿Es necesario algo más? No. Esto es un aporte. Agregaría que preferí JSON sobre XML por conocer mi primer destino: JavaScript, no sin antes investigar la posibilidad de utilziar ambos métodos de representación sin mayor complejidad.

Finalmente, y viendo que esto podía ponerse algo largo, me preguntaba si escribir varios Posts abordando los temas de forma incremental, o separar un solo Post en pequeños capítulos. Opté por la segunda opción para evitar al lector la necesidad de linkear diferentes Posts para obtener el concepto general. Claramente, estos capítulos son escritos en un orden particular, pero deberían ser salteados si el lector se considera cómodo con su contenido.
Intentaré mantener los highlights, bolds, etc para llamar la atención del lector cuando sea realmente necesario.

Comencemos entonces.

SOA y REST

RINS & SINR (REST is not SOA & SOA is not REST)

Es una pequeña aclaración que me permito hacer, no en defensa de uno ni de otro, sino como una forma de diferenciación. SOA (Services Oriented Architecture) es una arquitectura y metodología para el desarrollo de aplicaciones desacopladas (algo que ambos tienen en común). Pero cuando hablamos de SOA, también solemos mencionar que los servicios no son necesariamente Webservices. SOA puede hacer referencia a separación de capas de una aplicación, comunicación de aplicaciones de empresa (Enterprise Applications), Orquestación (generalmente utilizando motores de reglas de negocios), monitoreo (obtención de métricas, KPIs), procesos de negocio, y toda la suite. REST en cambio, apunta a un objetivo particular (y quizas menos ambicioso). Exponer servicios. O mejor dicho, exponer recursos.

REST

Partiendo de un concepto bien abstracto, REST basa su funcionamiento en la publicación de recursos. En una aplicación, un recurso suele coincidir con una entidad: Usuario, Cliente, Factura, Evento, etc. Puede que sea una sutileza, pero mientras que al utilizar SOAP, solemos definir el webservice mediante un WSDL de forma tal que el servicio sea una acción, a la que se le pasan parametros, devolviendo información (traerClientes(‘que hayan realizado compras en el año actual’)), REST intenta definir al Cliente como un recurso, y en última instancia, existirá una acción “TRAER” parametrizable. Pero no son los primeros ejemplos que suelen verse en REST. REST nos dice: Si queremos obtener todos nuestros clientes, llamaremos a …../clientes/ . Si queremos al cliente con ID 1, llamaremos a …../clientes/1 . Incluso, dentro de los primeros ejemplos, si queremos especificar un formato, lo hacemos como si nos refiriesemos a un archivo: cliente/1.xml o cliente/1.json (para no quedarnos en lo meramente dogmático, consideremos también cliente/1.csv y cliente/1.miformatopropietario). Finalmente, vemos como los ejemplos se asemejan a una URL web. Esto no es accidental, ya que el enfoque de REST, se basa en reutilizar el protocolo HTTP (y nos referimos principalmente a sus “métodos”) para conocer las operaciones a realizar sobre el recurso. De esta manera, considerando que mi aplicación corre en http://example.ourbit.com y la capa de servicios se encuentra en http://example.ourbit.com/services , podríamos establecer que:
GET http://example.ourbit.com/services/clientes/1.jsonObtiene el cliente con Id 1 y devuelve en formato JSON
GET http://example.ourbit.com/services/clientes/.xmlObtiene todos los clientes en formato XML
POST http://example.ourbit.com/services/clientes/parametro1/parametro2/.../parametroNCrea un nuevo cliente utilizando los parámetros
PUT http://example.ourbit.com/services/clientes/1/parametro1/parametro2/.../parametroN Modifica al cliente 1 utilizando los parámetros


De manera análoga, REST indica que deberíamos utilizar los HTTP status para indicar la respuesta (además de los datos, si existieran). Por ejemplo, si buscáramos el cliente con ID 1 y no lo encontráramos, deberíamos responder con un error 404. Si ocurriera un problema en la aplicación, lo correcto sería devolver un error 500. Si no se contara con privilegios suficientes para acceder a un recurso particular, el error 403 debería ser devuelto (por mencionar algunos ejemplos). Incluso, si la petición es respondida correctamente, esta respuesta debería incluir un HTTP Status 200. La devolución de estos códigos se hace mediante el seteo de los headers de HTTP. Cada lenguaje que implementa el protocolo, provee las funciones necesarias para hacerlo. Más adelante, veremos como lo hace PHP.
Dada esta vista general de REST, vale aclarar que, siendo este un modelo arquitectónico, implementarlo no es otra cosa que atender las peticiones HTTP, con el formato “convencional” y dar las respuestas esperadas.
Finalmente, y siendo este un concepto que va más allá de REST, la utilización de “Servicios”, en este caso “WebServices” nos permite desacoplar aplicaciones o incluso, distintas capas de una misma aplicación. Ej.: Un sitio Web puede acceder a toda la capa de negocios mediante la invocación de WebServices. Estos WebServices podrán ser consumidos también por una aplicación de escritorio, teléfonos celulares, o cualquier otro dispositivo.

CakePHP

Es un framework basado en el patrón MVC (o su adaptación WEB), de la familia de los denominados “Rapid Frameworks”. Implementa el modelo “Convention over Configuration”, lo que indica que se pueden seguir las convenciones definidas por el mismo para obtener de forma rápida una aplicación, pero puede extenderse su funcionalidad mediante configuración para ejecutar lógicas tan complejas como se desarrollen.
En forma resumida, el patrón MVC (del cual escribiré un POST aparte en algun momento), se basa en 3 componentes principales: Model-View-Controller.
- View: Componentes encargados del despliegue de la interfaz. Llaman al Controller para ejecutar “acciones” especificadas por el usuario.
- Controller: Actua de punto de entrada a funcionalidades, maneja las reglas de navegación, y ejecuta lógicas de validación, formateo de datos, etc.
- Model: Es el modelo en si mismo (conocido también como la “capa de negocios” de la aplicación).

Convención CakePHP

Una visión simplificada de CakePHP (quizas intentando entenderlo como una API a utilizar), consiste en respetar la convención indicada para crear los componentes de Model-View-Controller, e incluso las entidades en base de datos. De esta manera, por ejemplo, puedo crear la tabla “clients”, con los campos: “id”, “name”, “address”. Luego, una clase “Client” (comenzando con mayúscula y en singular, contenida en un archivo llamado client.php). Automágicamente, esta clase contendrá (aunque no aparezcan en el código), atributos correspondientes a los campos de la base de datos, y algúnos métodos del tipo “find”, “findBy”, etc. Esta clase, será parte del modelo (y de hecho, debe crearse en una carpeta particular llamada Model).
Ahora bien, para solicitar al modelo, una instancia particular de alguna entidad, debe haber algún componente que lo solicite. Según el patrón MVC, esto ocurrirá en el controller. En CakePHP, el controller es una clase (nomenclada en este caso como ClientsController, contenida en un archivo llamado clients_controller.php). Esta clase contendrá generalmente los métodos básicos que responden a un CRUD (Create, Read, Update, Delete), aunque podrían crearse tantos métodos como sean necesarios. Cabe destacar, que al acceder por URL a una funcionalidad montada sobre cakePHP, lo primero en ejecutarse es el controller, que llamará al model, y luego de setear alguna variable con su resultado, invocará a la vista correspondiente que sabrá realizar el despliegue de las entidades obtenidas.
Finalmente, la vista, es un archivo de extensión CTP, ubicado en la carpeta views/entidad. En este caso, views/client/index.ctp , corresponde a la vista por default a ser invocada por ClientController::index(). De manera sencilla, podemos definir la vista a ser invocada por un controller (sobreescribiendo la convención con configuración, como se explica en secciones posteriores).
Para la perspectiva REST, lo explicado sobre el MODEL no varía en absoluto. Las principales dependencias se darán con el Controller (una URL mapeará a un controller determinado), y el formato especificado en dicha URL, especificará la vista a invocar.

Rest sobre CakePHP

Creo que con lo ya dicho sobre Rest, y luego sobre CakePHP, la forma de implementar REST mediante CakePHP se cae de maduro. O al menos, es casi obvio lo que querríamos pedirle a la convención de CakePHP. Si me llega una URL del estilo: http://example.ourbit.com/services/clients/1.json , me gustaría que cakePHP invoque a ClientsController::view(1), y al invocar la vista, que solicite aquella que devuelva las entidades en formato JSON. Sin ningún framework de por medio, deberíamos realizar un parse de la URL, y en base a ello, llamar a alguna lógica. Esto, si bien no es demasiado complejo, puede ser laborioso y desde ya, dos aplicaciones diferentes solo tendrán un ciclo de vida convencional de casualidad.
Aquí está la primera buena noticia. CakePHP soporta REST. Basta incluir algunas líneas en los archivos de configuración para que el mapeo URL<->Controller ocurra. En tanto para las vistas, basta con que al realizar el despliegue, en lugar de dibujar un HTML, devuelva XML, JSON (o el formato requerido) y realice un cambio en el content-type (modificando el header HTTP).
Estos pasos se encuentran documentados en el sitio de CakePHP. Basicamente:
- Agregar al archivo .../cakephp/app/config/routes.php las líneas:
Router::mapResources('clients'); // para cada entidad que deseemos mapear.
Router::parseExtensions('xml','json'); // para definir los formatos soportados. Si no se especifica ninguno, XML es el formato soportado por defecto.
- Crear las vistas: Al crear las vistas, /views/clients/xml/method.ctp -> invocada para el formato XML del método method de ClientsController. Su contenido podría ser:

serialize($clients)?>


Importante (Esto no está correctamente documentado): Al hacer lo propio con otros formatos, por ejemplo JSON, podría no funcionar como esperado. Para completar la disponibilidad de un formato, se debe crear .../views/layouts//default.ctp (en este caso, formato sería json) y su contenido:

header("Pragma: no-cache");

header("Cache-Control: no-store, no-cache, max-age=0, must-revalidate");

header('Content-Type: application/json');

header("X-JSON: ".$content_for_layout);

echo $content_for_layout;

?>


Utilidades CakePHP

Reutilización de vistas

Al comenzar a desarrollar, noté que mi aplicación tenía distintos métodos que obtenían una lista de una entidad X. Por ejemplo:
- Obtener Clientes (todos).
- Obtener Clientes que hayan comprado en el último mes.
- Obtener Clientes que presenten deudas.

Estas tres operaciones, son atendidas por tres métodos distintos de ClientsController. Por defecto, CakePHP invocaría a tres vistas diferentes. Ahora bien, estas vistas harían lo mismo. Tomarían la variable que contiene la lista de clientes, y la devolverían en formato XML, JSON, etc.
Sería deseable entonces, tener una sola vista invocada por estos tres métodos. Para especificar manualmente, la vista a invocar (sobreescribiendo la convención con la configuración), basta con especificar antes del fin del método: $this->render('nombreDeLaVista'); (siendo la vista, nombreDeLaVista.ctp).

Reutilizar funcionalidades dentro de una vista

Es deseable también en algunos casos, poder incluir un “componente” dentro de diferentes vistas. Es decir, reutilizar cierta funcionalidad en varias vistas. Por ejemplo: Si quisiera imprimir en todos los XMLs un código común, o como en mi caso, setear el Access-Control-Allow-Origin para filtrar los dominios que pueden invocar un determinado servicio.
Para lograrlo, CakePHP nos provee del concepto de ELEMENTS. Dentro de la carpeta views, existe una subcarpeta llamada elements. Aquí puedo colocar miElemento.ctp, que es el archivo con la funcionalidad a reutilizar. Para invocarlo desde otras vistas, basta con incluir en las mismas: $this->element('miElemento'); (Sin la extensión ctp).
En el ejemplo de la aplicación, dentro de /views/elements, ubiqué el elemento acao.ctp siendo su contenido:

if (Configure::read('Security.acao.enabled')) {
if(!isset($_SERVER['HTTP_ORIGIN'])) {header("HTTP/1.1 403 Access Forbidden");die();}
$acaoDomains = Configure::read('Security.acao.domains');
header('Access-Control-Allow-Origin: ' . $acaoDomains[$_SERVER['HTTP_ORIGIN']]);
} else {
header('Access-Control-Allow-Origin: *');
}
?>

y en las vistas donde quería aplicarlo: $this->element('acao');

El contenido de la propiedad ‘Security.acao.enabled’ es seteado en .../cakephp/app/config/core.php de la siguiente manera:
Configure::write('Security.acao.domains', $acaoDomains);

Nota: No confiar en el método descripto, como una manera confiable de segurizar nuestros servicios. La variable de servidor ‘HTTP_ORIGIN’ es falseable de maneras muy sencillas.

Cliente jQuery

Esto no constituye una parte central de la temática planteada, pero es bueno plantear el ejemplo para ver la utilidad de realizar una orientación a servicios, o más precisamente: Publicar una API a ser consumida por un cliente.
REST, nos dió una forma sencilla de publicar una API. CakePHP nos proveyó de una implementación REST. Gracias a esto, podemos desarrollar distintas presentaciones de nuestra aplicación, reutilizando INTEGRAMENTE la lógica de negocio. jQuery podría considerarse como una capa de abstracción de JavaScript. Funcionalidades que antes eran complejas de desarrollar, con jQuery se simplifican. Un gran ejemplo de esto, es la manera de realizar llamadas AJAX. Simplemente:
$.ajax({
url: "clients/1.json", // especificamos la URL del servicio
success: function( data ) { // Al ser asincrónico, definimos la función a ejecutar cuando finalice la ejecución del serivicio. data, contendrá lo expuesto por el servicio.
response( $.map( data, function( item ) { // Mapeo de la data JSON en item
return {
label: item.Client.name,
value: item.Client.id,
}
}));
}
});

Como fue dicho anteriormente, esto es solo un ejemplo de como llamar a una URL mediante jQuery. Notese que desde el lado del cliente, es indistinto si del otro lado hay PHP, si se usa CakePHP, JAVA, o cualquier otra tecnología. Solo orientamos al servicio y a los datos devueltos.
Cambiar la capa de presentación NO IMPACTA EN ABSOLUTO los servicios expuestos.

Conclusión

No hace falta ser un experto para implementar una aplicación en CakePHP, y poder publicar su API respetando el modelo arquitectónico REST. Sin embargo, esta simple labor, nos permite implementar soluciones orientadas a servicios en las cuales la vista se encuentra totalmente desacoplada del modelo. Un equipo de desarrollo podría definir la API de la capa negocios, y luego desarrollar en paralelo distintos clientes e incluso dicho modelo.