Patrón Service Layer en Zend Framework
by Christian Soronellas on febrero 8, 2011
Introducción
Muchas veces las aplicaciones web se encuentran con la necesidad de implementar ciertas funcionalidades o características qué deben responder al navegador en diferentes formas. Un ejemplo de ello son las aplicaciones web funcionando con AJAX y solicitando datos al servidor de forma asíncrona sin bloquear la UI del navegador, para así poder ofrecer al usuario una mejor y más rica experiencia de usuario. Es por ello también, qué se produjo el nacimiento de las restful web applications (de las qué voy a hablar en venideras entradas :). Pero el objeto de esta entrada se centra en ofrecer una via de respuesta con formato JSON implementando el protocolo JSON-RPC 2.0 a través del componente, Zend_Json_Server de Zend Framework. Un método un poco más simple qué REST pero algo menos complicado que, bajo mi humilde punto de vista, puede servir de enfoque para proyectos web no demasiado grandes.
Patrón Service Layer
Tal cómo define Randy Stafford en el libro de Martin Fowler el patrón Service Layer define un límite de la aplicación con una capa de servicios que establece un conjunto de operaciones disponibles y coordina la respuesta de la aplicación en cada operación. En otras palabras, estaríamos hablando de una capa qué actuaría por encima del modelo de dominio y que proporcionaría los datos qué se le pidieran a través de los métodos qué definiera. Así, por ejemplo, podríamos tener un service de pedidos (para sólo obtener datos de pedidos), o un service de productos, o de usuarios, etc.
En este caso no voy a entrar en detalles acerca de cómo implementar otros formatos de respuesta, pues es bien sencillo a partir de revisar el ejemplo qué todo seguido voy a exponer.
Sí, sí, muy guapo … pero y ahora qué?
Pues todo seguido voy a poner un ejemplo de una manera cómo se podría implementar sin llegar a ser demasiado complicado. Cómo siempre va a estar basado en una instalación limpia de Zend Framework. Así qué lo primero es iniciar nuevo proyecto y lo podemos hacer mediante
> zf create project . > zf enable layout > zf configure db-adapter 'adapter=PDO_SQLITE&dbname=APPLICATION_PATH "/../data/db/database-development.db"' development > zf create db-table section Section > zf create model Section > touch data/db/database-development.db > sqlite3 data/db/database-development.db
Luego procederemos a crear la base de datos con la qué vamos a trabajar en el entorno de desarrollo.
CREATE TABLE section ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, title VARCHAR(50) NOT NULL, BODY TEXT NOT NULL );
Una vez creado el nuevo proyecto y la base de datos, procedemos a crear toda la capa qué interactuará con el Domain Model.
/**
* File located at library/Application/Services/Sections.php
*/
class Application_Services_Sections
{
/**
* The sections table instance
* @var Application_Model_DbTable_Section
*/
protected $_sectionsTable;
/**
* A setter to inject the sections table
*/
public function setTablesSection(Zend_Db_Table_Abstract $sectionsTable)
{
$this->_sectionsTable = $sectionsTable;
}
/**
* Returns a list of avaliable sections
* @return array
*/
public function getSections()
{
if (null === $this->_sectionsTable) {
$this->_sectionsTable = new Application_Model_DbTable_Sections();
}
return $this->_sectionsTable->fetchAll()->toArray();
}
/**
* Returns a single section by its ID
* @param integer $id The section ID
* @return array
*/
public function getSection($id)
{
if (null === $this->_sectionsTable) {
$this->_sectionsTable = new Application_Model_DbTable_Sections();
}
return $this->_sectionsTable->find($id)->current()->toArray();
}
}
De este modo queda definido nuestro punto de entrada a la aplicación por JSON-RPC, bajo el qué hay disponibles dos operaciones: getSections, qué devuelve una lista de secciones y getSection, qué dado un ID de sección devuelve su correspondiente registro. Ahora procederemos a crear el controlador encargado de proporcionar la respuesta de las llamadas por medio de JSON-RPC y sus correspondientes rutas para que la aplicación pueda responder por los diferentes servicios qué se le soliciten. Veamos.
/**
* File located at application/controllers/ServiceController
*/
class ServiceController extends Zend_Controller_Action
{
/**
* The JSON-RPC server instance
* @var Zend_Json_Server
*/
protected $_jsonServer = null;
/**
* The inflector used to map all the services to the classes correctly
* @var Zend_Filter_Inflector
*/
protected $_inflector = null;
/**
* (non-PHPdoc)
* @see library/Zend/Controller/Zend_Controller_Action::init()
*/
public function init()
{
/**
* Initializes the controller, and disable the viewRenderer and the layout
* only for this controller. Initializes the inflector and then starts the
* Zend_Json_Server.
*/
$this->_helper->getHelper('viewRenderer')->setNoRender(true);
$this->_helper->getHelper('layout')->disableLayout();
$this->_inflector = new Zend_Filter_Inflector(':classname');
$this->_inflector->setFilterRule(
'classname', array('Word_UnderscoreToDash', 'Word_DashToCamelCase')
);
$this->_jsonServer = new Zend_Json_Server();
}
/**
* Prepares the JSON-RPC server with the service (class :) requested. The reques
* ted service should exist on the "library/Application/Service/ServiceName.php",
* being "ServiceName" the service name.
* For example, to call an operation form the service "order-detail", the file
* "library/Application/Service/OrderDetail.php" must exist.
* @param string $service The requested service
*/
protected function _prepareJsonRpcServer($service)
{
$this->_jsonServer->setClass(
'Application_Service_' . $this->_inflector->filter(
array('classname' => $service)
)
);
}
/**
* Handle a JSON-RPC Request
*/
public function indexAction()
{
$this->_prepareJsonRpcServer($this->getRequest()->getParam('service'));
$this->_jsonServer->handle();
}
/**
* Return the Service descriptor
*/
public function smdAction()
{
$service = $this->getRequest()->getParam('service');
$this->_prepareJsonRpcServer($service);
$this->_jsonServer->setTarget('/services/' . strtolower($service))
->setEnvelope(Zend_Json_Server_Smd::ENV_JSONRPC_2);
$this->getResponse()->setHeader('Content-type', 'application/json');
$this->getResponse()->setBody($this->_jsonServer->getServiceMap());
}
}
Este controlador, tiene 2 propiedades. Una para albergar una instancia del servidor de JSON-RPC y otra para albergar una instancia del mapper que actuará de puente entre los servicios disponibles y sus classes correspondientes. Además definimos dos posibles acciones: la acción por defecto, desde la qué se van a realizar todas las operaciones (indexAction) y otra acción (smdAction) para enviar un descriptor del servicio a quién lo solicite. Para la ejecución de todas las acciones, vamos a decirle a este controlador qué no haga render de la vista y que no la vista con su layout correspondiente. Con el controlador definido, ahora sólo queda definir las rutas correspondientes.
; File located at application/configs/routes.ini ; Application routes ; The main services endpoint routes.service.route = "services/:service" routes.service.defaults.controller = service routes.service.defaults.action = index ; The service descriptor route routes.smd.route = "services/service-descriptor/:service" routes.smd.defaults.controller = service routes.smd.defaults.action = smd
Añadimos una entrada en el archivo de bootstrap para qué se haga uso de las rutas en la aplicación
/**
* File located at application/Bootstrap.php
*/
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
public function _initApplicationRoutes()
{
$this->bootstrap('frontcontroller');
$this->getResource('frontcontroller')
->getRouter()
->addConfig(
new Zend_Config_Ini(
APPLICATION_PATH . DIRECTORY_SEPARATOR . 'configs' . DIRECTORY_SEPARATOR . 'routes.ini'
),
'routes'
);
}
}
Ya con casi todo el tinglado montado, sólo nos queda montar la parte del cliente qué va a contactar y consumir los servicios disponibles. Para ello, yo he implementado un cliente muy muy sencillo a modo de ejemplo a través del plugin de jQuery, jQuery JSON RPC 2.0. Así qué bajamos una copia del proyecto y copiamos el archivo jquery.jsonrpc.js a la carpeta js (sino existe la creamos) de nuestro directorio public. Además allí también crearemos el archivo site.js con el siguiente código.
$(document).ready(function() {
$('a').click(function(event) {
$.jsonRPC.setup({
endPoint: '/services/sections'
});
$.jsonRPC.request('getSections', {}, {
success: function(result) {
var html = '<ul id="sections" class="list">';
$.each(result['result'], function(index, section) {
html += '<li class="section">' + section.title + '</li>';
});
html += '</ul>';
$(document.body).append(html);
}
});
event.preventDefault();
});
});
Y ya para finalizar, sólo hace falta añadir una interacción en la presentación del controlador IndexController para poder demostrar todo este comportamiento. Además también añadiremos las llamadas necesarias a los correspondientes archivos de javascript para qué sea efectiva la demostración.
<!-- File located at application/layouts/scripts/layout.phtml -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN"
"http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">
<html>
<head>
<title>JSON RPC Test</title>
</head>
<body>
<?php echo $this -> layout() -> content; ?>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
<script type="text/javascript" src="/js/jquery.jsonrpc.js"></script>
<script type="text/javascript" src="/js/site.js"></script>
</body>
</html>
<!-- File located at application/views/scripts/index/index.phtml --> <p><a href="#">Get a list of sections</a></p>
Concluyendo
Con todo lo dicho hasta ahora deberíamos ser capaces de poder invocar métodos remotos mediante JSON-RPC. Y esto puede ser extrapolado a lo que se quiera hacer. Es decir, si tenemos un e-commerce del qué queremos conocer el detalle de un pedido asíncronamente, esta puede ser una manera (no la única) de hacerlo ordenadamente, qué a mi particularmente me gusta. Y no soy el único puesto qué sitios más grandes con altos volumen de tráfico (Facebook, Tuenti, etc.) ya lo estan llevando a la práctica (esto y algunas cosas más).
Cómo siempre, voy a dejar una copia del código colgada en mi cuenta de github para quién le interese poder trastear un poco con todo esto.


Inglés
Español
One comment
Se que el post tiene ya un año de antiguedad, pero te pregunto igual, he seguido los pasos y no me anda cuando quiero ejecutar el método de la clase en mi caso una de Test que devuelve sólo string. Me sale el siguiente error:
{“result”:null,”error”:{“code”:-32600,”message”:”Invalid Request”,”data”:null},”id”:null}
pasa cuando se ejecuta:
$this->_jsonServer->handle();
Corregí algunas cosas de tu código e incluso me bajé de github para tenerlo igual y no me funciona.
Estoy trabajando con SSL tal vez sea ese el problema, se te ocurre alguna idea??
Saludos
by Daniel on 08/02/2012 at 21:32. #