Migrar aplicación legacy a Symfony2: PSR-0

Introducción

Después de algún tiempo sin postear, me he decidido a retomar mi actividad blogera. Y para ello me he decidido a hablar un poco sobre aplicaciones legacy y cómo podemos hacer para mejorarlas. Así que voy a empezar una serie de entradas, relativas a como podemos migrar una aplicación legacy hacia Symfony2. Sí, Symfony2. Quizá algunos se pregunten porqué Symfony2, si la mayor parte de entradas que había hecho hasta ahora eran acerca de Zend Framework. La verdad es que después de ver el nacimiento de Zend Framework 2 y de trastearlo un poco … He quedado un poco desilusionado. Zend Framework 2 es un muy buen framework, un excelente framework, eso es indudable. Pero después de usar Symfony2 y sus componentes la sensación final que me queda es que hubo unos señores que se sentaron y se propusieron desarrollar un buen framework. Por otra parte hubo unos señores que se sentaron y se propusieron desarrollar el mejor framework del mercado. Aunque me consideraba fanboy de Zend Framework, tengo que reconocer que el sr. Potencier y compañía han hecho un trabajo para sacarse el sombrero.

¿PS-Qué?

Lo primero que vamos a tratar va a ser de los naming conventions de clases. Si aún no te suena, el acrónimo PSR significa “PHP Standards Recomendation” y son una serie de estándards propuestos por el groupo PHP-FIG. PHP-FIG es el acrónimo a su vez de PHP Framework Intergroup y es un grupo que congrega a los principales creadores de frameworks y se sientan a debatir y proponer estándares para unificar ciertos aspectos del desarrollo en PHP. La verdad que ya era hora que naciera un grupo así.

La primera recomendación que sacó PHP-FIG fue el PSR-0 que define una convención para unificar los nombres de las clases y permitir la interoperabilidad de autoloads. Para ello dada una clase se establece una relación 1:1 con un archivo físico mediante las siguientes reglas

  • Los nombres de clase deben tener una estructura cómo esta: \Vendor\(Namespace)*\Clase dónde pueden definirse tantos Namespaces cómo se quiera y vale tanto para namespaces de PHP 5.3 como para prefijos de clase al estilo PEAR (Vendor_Namespace_Clase). Ejemplos válidos de ello serían: Symfony\Component\HttpFoundation\Request o Zend_Controller_Front.
  • Todos los nombres de clase deben tener un primer nivel de namespace (Vendor: Zend o Symfony siguiendo la pauta del ejemplo del primer punto)

Vale. Muy bonito todo. Pero cómo lo hago?

Muy sencillo, puedes usar el componente de autoloading de Symfony que soporta a rajatabla todo el PSR-0 y además añade algunos extras muy interesantes que nos pueden ayudar. Para ello y paso previo a usar el componente debemos cerciorarnos que nuestras clases cumplen el PSR-0 estricto. Luego, una vez lo hemos descargado e instalado, el componente nos provee con la clase Symfony\Component\ClassLoader\UniversalClassLoader que nos permite cargar cualquier clase que siga el PSR-0.

<?php

require_once __DIR__.'/src/Symfony/Component/ClassLoader/UniversalClassLoader.php';

use Symfony\Component\ClassLoader\UniversalClassLoader;

$loader = new UniversalClassLoader();
$loader->registerNamespaces(array(
    'Symfony'          => array(__DIR__.'/src', __DIR__.'/symfony/src'),
    'Doctrine\\Common' => __DIR__.'/vendor/doctrine-common/lib',
    'Doctrine'         => __DIR__.'/vendor/doctrine/lib',
    'Monolog'          => __DIR__.'/vendor/monolog/src'
));

$loader->registerPrefixes(array(
    'Zend_' => __DIR__.'/vendor/zendframework/zf1/src'
));

$loader->register();

Mejorando el rendimiento del ClassLoader

Incluso si quisiéramos mejorar el rendimiento del ClassLoader, el propio componente nos provee de herramientas para hacerlo. En este caso, hablamos de la clase Symfony\Component\ClassLoader\ApcUniversalClassLoader. Esta clase a medida que vaya resolviendo los nombres de clase, los irá almacenando en APC (también existe su homologo en XCache) para luego en sucesivas requests no tener que volver a hacer el lookup de la clase.

<?php

require 'vendor/symfony/src/Symfony/Component/ClassLoader/UniversalClassLoader.php';
require 'vendor/symfony/src/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php';

use Symfony\Component\ClassLoader\ApcUniversalClassLoader;

$loader = new ApcUniversalClassLoader('apc.prefix.');
 
// register classes with namespaces
$loader->registerNamespaces(array(
    'Symfony\Component' => __DIR__.'/component',
    'Symfony'           => __DIR__.'/framework',
    'Sensio'            => array(__DIR__.'/src', __DIR__.'/vendor'),
));
 
// register a library using the PEAR naming convention
$loader->registerPrefixes(array(
    'Swift_' => __DIR__.'/Swift',
));
 
// activate the autoloader
$loader->register();
<?php

require 'vendor/symfony/src/Symfony/Component/ClassLoader/UniversalClassLoader.php';
require 'vendor/symfony/src/Symfony/Component/ClassLoader/ApcClassLoader.php';

use Symfony\Component\ClassLoader\UniversalClassLoader;

$loader = new UniversalClassLoader();

// register classes with namespaces
$loader->registerNamespaces(array(
    'Symfony\Component' => __DIR__ . '/component',
    'Symfony'           => __DIR__ . '/framework',
));

$loader->registerPrefixes(array(
    'Zend_' => __DIR__ . '/zend'
));

$cachedLoader = new ApcClassLoader('my_prefix', $loader);

// activate the cached autoloader
$cachedLoader->register();

// eventually deactivate the non-cached loader if it was registered previously
// to be sure to use the cached one.
$loader->unregister();

Conclusión

Hemos empezado a migrar nuestra aplicación legacy a través de estandarizar y unificar los nombres de clase de nuestra aplicación. Y lo hemos hecho a través del PSR-0 y del componente ClassLoader de Symfony. Si bien seguramente podrán existir métodos para hacer autoloading seguramente más eficientes, este nos provee una manera fácil e intuitiva para dar nombres a las clases que nos permitirá dado un nombre de clase saber su ubicación exacta casi al instante dentro de nuestra base de código, además de saber exactamente cómo se va a cargar.

Para el siguiente post voy a tratar acerca de cómo podemos manejar dependencias de librerías dentro de proyectos y qué papel juega el PSR-0 con el ClassLoader de Symfony en ello.

Autoloading en Zend Framework

Últimamente en emagister, me encuentro dedicado entre otras cosas a ajustar el rendimiento del autoloading de clases en Zend Framework ya qué es una posible mejora que puede reportar un enorme ROI en impacto de negocio. De acuerdo, quizá no directamente en impacto de negocio pero sí de manera indirecta. Probablemente un sitio web que pueda servir más páginas a más usuarios puede ver sensiblemente aumentado sus ratios de conversión. Y eso, creo que puede poner contentos a los chicos del área de negocio. Bien, empecemos.

El componente Zend_Loader

Este quizá sea uno de los componentes más usados en todo el framework, ya que cómo su propio nombre indica es el encargado de toda la carga de clases. Pero una de las cosas que más me desagrada, ya no del componente sino del propio framework en sí, es la manera qué tiene de usarlo, pues algunos de sus subcomponentes (cómo el Zend_Loader_PluginLoader) son instanciados directamente por otras clases produciendo un pequeño acople que a veces resulta un poco molesto. Y más si te dedicas a investigar cómo mejorar precisamente este aspecto.

Pros y contras de usar Zend_Loader

Por una parte, Zend_Loader es un componente muy cómodo para desarrollar sobretodo si estamos usando Zend_Loader_Autoloader. Tiene una integración perfecta con Zend_Controller_Front permitiendo la carga de clases automática dentro de módulos MVC, lo que cómo he dicho es muy cómodo pues el desarrollador ya no tiene que preocuparse de la carga de clases y puede centrarse en otros aspectos de la aplicación.

Cómo contra, está la penalización en rendimiento que puede ocasionar el utilizar según que partes. Por ejemplo, usar Zend_Loader::loadClass tiene una fuerte penalización en rendimiento debido a que siempre tiene que calcular el path a la clase que se le está pidiendo. Cálculos cómo diferenciar un namespace de PHP 5.3 de un namespace de PEAR, comprobar que ciertos caracteres no aparezcan en el nombre de archivo, jugar con el include_path y finalmente incluir la clase. Por suerte este componente ya ha sido deprecado a partir de la versión 1.8 de Zend Framework y a partir de la versión 2.0 va a dejar de existir, además de que se desaconseja encarecidamente su uso en favor del componente Zend_Loader_Autoloader.

Otro componente qué también penaliza bastante en rendimiento es el Zend_Loader_PluginLoader, es decir el responsable de la carga de plugins de Zend Framework y ampliamente usado (y acoplado :) por otros componentes. Este componente es capaz de registrar “prefijos” de classe y por cada uno de ellos asociarle uno o más paths donde puede existir el componente. El coding standards de Zend Framework dicta que los prefijos de clase sería todo el árbol de carpetas en el que se encuentra el componente separados por “_”. Así por ejemplo en el componente Zend_View_Abstract el prefijo de clase sería “Zend_View”. una vez visto esto y analizando un poco a fondo el componente Zend_Loader_PluginLoader se puede ver que al intentar cargar un plugin se recorre todos los prefijos de clases y sus paths asociados buscando un clase que encaje (por medio de Zend_Loader::isReadable, qué también penaliza). Por ejemplo para cargar el plugin (inventado) “Css” teniendo los prefijos registrados “Mi_Libreria_Personalizada” y “Zend_View_Helper” con 5 paths por cada prefijo, podría llegar a hacer 10 entradas en disco por cada petición HTTP.

Tips para mejorar el autoloading en Zend Framework

Voy a hacer una breve síntesis de los métodos qué existen para mejorar la carga de clases y de plugins en Zend Framework. Todo este material se puede encontrar en el sitio web de Zend Framework y por Google, y aquí lo voy a agrupar un poco.

Optimizar el include_path

Hay varias cosas que se pueden hacer para optimizar el include_path. En primer lugar, siempre usar paths absolutos o paths relativos a un path absoluto para hacer mayor uso de las directivas realpath_cache_*, ya que así no obligamos al intérprete de PHP a resolver el path relativo y además vamos a ganar algo en rendimiento ya que vamos a hacer que internamente PHP use el cache de paths absolutos.

<?php
 
define('APPLICATION_PATH', realpath(dirname(__FILE__)));
$paths = array(
    APPLICATION_PATH . '/../library'),
    '.',
);
set_include_path(implode(PATH_SEPARATOR, $paths);

Además también podemos reducir el numero de paths dentro de include_path para reducir las posibilidades de búsqueda de clases, definir el path a Zend Framework lo antes posible en la lista de paths de include_path y/o no definir el directorio actual cómo directorio de búsqueda en include_path. Así que si aún no sigues estas reglas, ya lo sabes! ;)

Eliminar require_once innecesarios

Las aplicaciones web actuales deberían todas hacer uso del autoloading de clases. Si aún no lo estás usando, plantéatelo pues es algo bastante necesario ya que unifica los procesos de carga de clases y ayuda a establecer un orden de directorios.

Zend Framework por defecto en casi todas las definiciones de clase usa los require_once, y usando el componente Zend_Loader_Autoloader ya no son necesarios. Y no solo eso sino que de esta manera estamos usando lazy loading para la carga de clases lo que sin duda nos va a dar alguna mejora de rendimiento.

# Script para remover los "require_once" innecesarios en *nix
cd path/to/ZendFramework/library
find . -name '*.php' -not -wholename '*/Loader/Autoloader.php' \
     -not -wholename '*/Application.php' -print0 | \
     xargs -0 sed --regexp-extended --in-place 's/(require_once)/\/\/ \1/g'
# Script para remover los "require_once" innecesarios en mac
cd path/to/ZendFramework/library
find . -name '*.php' -not -wholename '*/Loader/Autoloader.php' \
     -not -wholename '*/Application.php' -print0 | \
     xargs -0 sed -E -i '.bak' 's/(require_once)/\/\/ \1/g'

Mejorar la velocidad del PluginLoader

Cómo ya he dicho antes, el PluginLoader es un componente que permite registrar prefijos de clases con múltiples paths. Esto penaliza en rendimiento pues por cada solicitud de carga de plugins se debe examinar cada prefijo con cada path, para encontrar la clase. Aún así el componente Zend_Loader_PluginLoader puede crear una cache de plugins, es decir crea en tiempo de ejecución un archivo PHP dónde va añadiendo includes de todos los plugins de los que ya ha resuelto el path. Así que yo aconsejaría encarecidamente usar esta característica para entornos de producción (no tiene sentido ponerlo en entorno de desarrollo a no ser que se quiera medir el rendimiento de la aplicación).

<?php
 
$classFileIncCache = APPLICATION_PATH . '/../data/pluginLoaderCache.php';
if (file_exists($classFileIncCache)) {
    include_once $classFileIncCache;
}
 
if ($config->enablePluginLoaderCache) {
    Zend_Loader_PluginLoader::setIncludeFileCache($classFileIncCache);
}

Aún sigo necesitando mejor rendimiento en carga de clases, qué puedo hacer?

Si aún así, después de estos tips necesitas mejorar la carga de clases aún existe algún improvment más que se puede usar. En la versión 2.0 de Zend Framework, han mejorado el componente relativo a la carga de clases para sacar las partes deprecadas y para mejorar sustancialmente el rendimiento. Lo bueno es que este componente ya es estable y puede ser usado.

Uno de los puntos del nuevo desarrollo del componente de autoloading en Zend Framework ha sido el crear el autoloader más rápido posible, es decir aquél que no tenga que calcular nada para conocer la ubicación de una clase pero que aún así sepa dónde están ubicadas todas las clases que se van a usar. Esto se ha traducido en un mapa de clases.

En este caso, lo que propone Zend es que antes de hacer un deploy a producción se generen estos mapas de clases (un archivo con un array de clases cómo claves y paths cómo valores) para que la nueva clase ClassMapAutoloader (al fin y al cabo es un “autoloader tonto”) pueda disponer de ellos y mejorar el rendimiento. Si esto lo combinamos con APC que ya de por sí sin hacer nada acelera las aplicaciones web en PHP, tendríamos que poder ver una mejora sustancial del rendimiento.

Y cómo lo puedo usar en mi Zend Framework 1.x?

Pues bien los chicos de Zend Framework se han encargado de hacer un mini port para Zend Framework 1.x y se puede localizar en el github personal de Matthew Weier O’phinney, Techlead de Zend Framework, dónde se explica detenidamente cómo usarlo.

Y ya estoy Certificado en PHP 5.3!

A día de ayer, cosa de las 09.30h de la mañana, realicé el examen de certificación de PHP 5.3. Mis impresiones, a decir verdad, fueron que me esperava el examen un poco más difícil de lo qué realmente fue. Aunque sí qué es verdad qué había preguntas oscuras qué entrañaban cierta dificultad, creo que muchas de las preguntas podían sacarse usando la táctica de eliminación.

Pocas preguntas específicas de PHP 5.3

Una de las cosas que más me llamó la atención fue que preguntas específicas a PHP 5.3 no hubo más que tres: Una de namespaces y de late static bindings bastante sencillas y una de closures un poco más rebuscada. Pero realmente, eché en falta más preguntas relativas en PHP 5.3

Otros temas que se tocaron en el examen

Son los que ya se mencionan en las guías de Zend. Específicamente, de Databases me cayeron un par y las dos sobre transacciones bastante sencillas todo hay que decirlo. De fechas solo me cayó una, también sencillita. De XML, nada más que un par o tres de preguntas. Sobre todo hicieron foco en los PHP Basics, en el tema de Arrays, Web Features, algunas de Streams y de Security.

Mi recomendación

Para todos aquellos qué estén leyendo esta entrada y quieran sacarse la certificación de PHP 5.3, sobretodo hacer foco en los puntos en los qué he hecho referencia en el párrafo anterior, obviamente sin olvidar los otros. Por otra parte, no creo qué sea indispensable pero si muy útil, y es que una semana antes de hacer el exámen tuve acceso a unos videos sobre formación en certificación PHP 5.3 que proporciona Zend. Si podéis acceder a ellos de alguna manera los recomiendo por qué tratan preguntas parecidas a las del examen y sirven para ver en lo que se está fallando.

Certificacion PHP 5.3

PHP 5.4 – El espectáculo continúa

Muchos todavía estamos descubriendo las bondades de PHP5.3 y sus novedades y aún hay muchos sitios importantes que corren sobre PHP5.2. Sin embargo, el equipo que hace PHP no descansa y recientemente han publicado la primera versión alpha de lo que será PHP5.4 que promete muchas mejoras tanto en rendimiento como en funcionalidades.

Mi compañero Ricard Clau y yo hemos estado haciendo algunas pruebas con esta versión y aquí os presentamos algunas de las novedades que consideramos más destacadas.

Traits

Probablemente el plato estrella de la versión. Un trait viene a ser algo así como un trozo de clase que podemos importar en varios sitios. Además, en una clase hija, podemos heredar de varios traits. Y con esto, en parte, conseguimos la tan deseada por algunos herencia múltiple en cierta manera.

<?php
 
trait Singleton
{
    private static $_instance;
    private function __construct(){}
 
    public function __clone()
    {
        throw new Exception('This object cannot be cloned!');
    }
 
    public static function getInstance()
    {
        if (null === static::$_instance) {
            static::$_instance = new static();
        }
 
        return static::$_instance;
    }
}
 
class Product
{
    use Singleton;
}
 
$product = Product::getInstance();

Más fácil aún hacer Singletons y sobretodo, centralizado el código de instanciación.

Podéis ver algunos ejemplos más con, por ejemplo, tratamiento de colisiones al hacer el use de varios traits con funciones idénticas y algunos otros ejemplos en esta web.

Array Deferencing

Esto es algo bastante molón que hasta ahora no se podía hacer. Cuántas veces habéis tenido que llamar a una función que devuelve un array y al tener que tratar su resultado habéis pensado: Qué bien me iría poder poner corchetes al resultado de la función directamente. Pues eso por fin se puede hacer:

<?php
 
echo call_user_func(function(){ return array('test', 'deferencing'); })[1];

Aquí veríamos un ‘deferencing’ en consola. Muy a lo JavaScript.

DTrace

El DTrace es un sistema de debug de aplicaciones en tiempo real en entornos de producción. Usa scripts en “D” para debuggar partes de la aplicación al alcanzar ciertas condiciones (qué X archivo se ha abierto, qué Y linea se ha alcanzado, etc. ) y así aportar visibilidad en entornos de producción. Lejos ha quedado aquel tiempo en que las webs eran solamente una sección de noticias y un triste catálogo. Y a veces, tenemos aplicaciones monstruosas donde es complicado ver la secuencia de llamadas. Seguro que esto ayuda a encontrar errores en nuestra aplicación.

Mejoras en memoria y rendimiento de Zend Engine

Este punto es súper importante que no se descuide nunca. Bien es cierto que con los cacheos (HTTP_Cache, CDNs varios, Memcached, etc.) con APC para Opcode Cache y el resto de técnicas de high-performance se optimiza mucho. Pero al final, optimizando el core de PHP se ganará muchísimo ya que por la naturaleza de PHP, cada Request vuelve a empezar todo y el proceso de bootstraping debe ir lo más rápido posible.

Paso a PECL de la extensión de SQLite

Cada vez se usa más SQLite, para testear la BBDD, para entornos de desarrollo locales y es importante tener claro cómo trabajar con ella y cómo instalar la extensión para PHP.

Rotura con algunas miserias del pasado

Desaparecen por fin settings de tan infausto recuerdo como safe_mode, register_globals (cuánto daño ha hecho), register_long_arrays, allow_call_time_pass_reference, etc.

También desaparecen las siguientes funciones relacionadas con el tratamiento de sesiones: session_is_registered(), session_register() and session_unregister()

Además, el tema del break y el continue, queda de la siguiente manera: podemos hacer break y continue para salir de un bucle, break 2 o continue 3 para saltarnos varias iteraciones pero no podemos hacer un break $i. Nos dará un error de algo así como Expected Constant Value.

<?php
 
public function testBreakI()
{
    $z = 2;
    for($i = 0; $i < 10; $i++)
        for($j = 0; $j < 10; $j++)
            if ($i == 2 && $j == 2)
                break $z;
 
    var_dump($i, $j);
}
 
testBreakI();

Y en php.ini, el default encoding es UTF-8, el short tag de template <?= estará habilitado siempre y algunas cositas más que podéis ver en PHP.net

Conclusión

Es para mí, notícia de gran alegría, ver cómo un lenguaje al qué le profeso auténtica devoción va evolucionando día a día. Desde aquí dar las gracias a toda la comunidad de PHP por hacerlo más grande versión a versión. Y qué siga así por mucho más.

Si bien es cierto qué siempre hay cosas qué mejorar, pues soy firme creyente en qué siempre hay margen de mejora, cómo por ejemplo el tema de la superpoblación de frameworks o la perversión de CMSs.

Pero … que grande qué es PHP!

Acerca de los ciclos de desarrollo, respetuosamente.

Recientemente me he visto envuelto en una discusión de alto nivel en LinkedIn, pues un compañero y amigo de allí dónde trabajo me recomendó qué le echara un vistazo por ciertos comentarios vertidos en ese hilo. El título del hilo, JQuery or YUI? Pro’s and Cons. Dentro del grupo Front end Developer Group. Un interesantísimo debate. Lo recomiendo a quién me esté leyendo, tenga facilidad con el inglés y sobretodo le interesen temas de front-end.

Está mi ciclo de desarrollo en … peligro?

A lo largo de este debate cierto individuo, tengo qué decir con toda sinceridad que equivocadamente, pone por delante de cualquier otra decisión técnica el valor qué tiene la velocidad de ejecución cuándo se trata de decidir usar un framework de JS. Si ya te has puesto las manos a la cabeza, por favor, espera a acabar de leerme.

Es evidente qué la velocidad es un factor a tener en cuenta cuándo hemos de decidir usar una librería u otra. Pero nunca puede ser el único motivo por el cuál decantarse. Estaríamos incurriendo en una mala decisión, qué nos puede llegar a afectar muy negativamente en un futuro, ya qué las decisiones técnicas deben venir siempre motivadas por unas necesidades de negocio. Estás necesidades responden a la medición de unos costes sobre las mismas oportunidades de negocio qué nunca se pueden menospreciar. Es decir, me explico. Cuál puede ser el coste, para un negocio, de perder una oportunidad? Un gran oportunidad? Puede suponer perder el hecho posicionarse delante de la competencia en una posición realmente privilegiada. Luego la propia marca empieza a perder valor (o le cuesta horrores recuperarlo), delante de la competencia. Y esto, sólo sería el principio de la cadena de consecuencias. Y eso lo puede solucionar un factor cómo la velocidad de ejecución de una librería JS? Esto qué parece tan mezquinamente lógico, para ciertos individuos no lo es. Se debe ir con calma y humildad.

Qué impacto puede tener esto en el ciclo de desarrollo?

El caso más simple y más fácil de ver, es el del típico tío de negocios, cliente, Product Owner qué quiere ver lo qué ha solicitado a desarrollo, online lo antes posible. Obviamente, su misión será presionar para conseguirlo cuánto antes. Hasta aquí, nada raro. Nada qué no se viva en una empresa cotidianamente. Pero qué pasa cuándo por ejemplo, el diseño de esa librería tan rápida no permite desarrollar la nueva funcionalidad tan rápidamente? O cuándo la curva de aprendizaje es realmente pronunciada? O cuándo resulta qué cómo consecuencia del pésimo diseño de ese framework, tenemos algo muy costoso de mantener? Pues qué ese ciclo de desarrollo se puede ver incrementado sensiblemente, debido al coste de implementar/mantener la nueva funcionalidad. O de aprender cómo funciona la librería. O un largo etcétera.

Y ¡Carámba! Ahora a negocio, ya no le sirve de nada qué usemos algo realmente ultrarápido, pues todo lo qué ganamos con un aspecto, lo perdemos irremediablemente al perder esa oportunidad de negocio. Después, de su pertinente tirón de orejas. E incluso, dependiendo de la oportunidad perdida, nuestro puesto de trabajo.

Conclusiones finales

Elegir una herramienta de trabajo, no es un juego. Es algo de suma importancia y no debe nunca ser tomado cómo tal. De ello puede depender toda una empresa, por lo qué se deben considerar todos los requerimientos impuestos por negocio. Sin excepción.

Patrón Service Layer en Zend Framework

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

cd my/project
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.

<?php
 
/**
 * 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.

<?php

/**
 * File located at application/controllers/ServiceController.php
 */
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

<?php

/**
 * 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.

Zend Framework, Zend View and Twig

Introduction to PHP template engines

In order to contextualize this post I have to talk a bit about PHP template engines. Template engines are libraries intended to abstract all of an application views looking for the maximum eficiency and the best code reuse. If we have product listings, for example, on different categorizations, It allow us to define a common UI for the categories. This gives more flexibility to the application, and gives sense to the V of MVC. All thanks to the output control that PHP provides of (which was impossible with the old ASP, for example).

There are a lot of template engines in PHP. If we search on google, we will see it. But the de-facto standard till now was Smarty. Yes. I said was. Because until now, several template engines have been released which, on my point of view, have questioned Smarty as the PHP template engine by excellence. One of them, and I’m going to point the object of this entry, is Twig.

Smarty 3 vs Twig

Why not to use Smarty 3

All told. Smarty is a great library to separate the view and the application logic. It accomplishes its objective and accomplishes it, more than satisfies. However, It’s second version became outdated with PHP 5 and his new object handling model. Because of this, Smarty 3 was released. Among the new features of Smarty 3, with those of the previous version, we can emphasize:

  • Rewritten for PHP 5
  • A real syntax lexer (its previous version didn’t have one)
  • Template inheritance
  • Variable autoescaping
  • Function definition on template level

And there are more features. Maybe the question now could be, and what’s wrong? Seems that all said until now looks good. But what about the performance? How is going to affect to our application performance the inclusion of one or another engine? To answer this question I recommend the reading of a Fabien Potencier post, where stablishes a comparative between PHP template engines, and each draw its own conclusions. At the end, each one knows the project that he/she is developing and each one knows what is appropiate. I’ll stick with Twig.

Why yes to use Twig

First of all, and as I said before, because performance reasons. As told in Fabien Potencier’s post there are performance differences between the two engines. But beyond that, the decision to use this component can imply another aspects of itself. Because of this, all that follows this comes basically from my point of view.

Secondly, this engine comes with features already developed that doesn’t come shipped with Smarty, besides all the features of Smarty that already Twig integrates. One of them, and I have to say that I like a lot, is its Sandbox mode. The Sandbox mode, allows to restrict which features the template editor will be allowed to use.. I refer to for example: If I don’t want the web designer to use the if structure, by the Twig Sandbox mode, he/she won’t be allowed to use it. An this is for any feature of the engine. Even Twig allows to restrict features of our model classes. Almost impressive.

Thirdly and for the last, I prefer the Twig extensibility model more over than the Smarty one. I believe it’s easier to add functionality to Twig. Smarty, on its third version, still comes with the Smarty 2 plugin model and sincerely, with PHP 5.3 rising, I miss a more object oriented extensibility model.

Zend_View and its agnosticism

I say agnosticism because it’s one of the component strongest feature. Zend_View it’s template engine agnostic, mostly for the Zend_View_Interface component, becoming PHP itself the template engine by default. And that will be, on this post, the way I’m going implement Twig as a Zend_View extension and override the default instance of Zend_View on runtime, by our (already known :) abstract class Zend_Application_Resource_ResourceAbstract implementation.

<?php

// File located at "library/Twig/Zend/View/Twig/TwigImplementation.php
require_once 'Zend/View/Interface.php';
require_once 'Twig/Autoloader.php';

class Twig_Zend_View_Twig_TwigImplementation
    implements Zend_View_Interface
{
    /**
     * The twig loader instance
     * @var Twig_Environment
     */
    protected $_twigEnv;

    /**
     * The twig loader instance
     * @var Twig_Loader_Interface
     */
    protected $_twigLoader;

    /**
     * A map of all registered variables
     * @var array
     */
    protected $_variablesMap = array();

    /**
     * Class constructor
     *
     * @param string $templateBasePath The views base path
     * @param array $envOptions
     */
    public function __construct($templateBasePath, array $envOptions = array())
    {
        $zendAutoloader = Zend_Loader_Autoloader::getInstance();
        $zendAutoloader->pushAutoloader(array('Twig_Autoloader', 'autoload'));

        $this->_twigLoader = new Twig_Loader_Filesystem($templateBasePath);
        $this->_twigEnv = new Twig_Environment($this->_twigLoader, $envOptions);
    }

    /**
     * (non-PHPdoc)
     * @see library/Zend/View/Zend_View_Interface::getEngine()
     * @return Twig_Environment
     */
    public function getEngine()
    {
        return $this->_twigEnv;
    }

    /**
     * (non-PHPdoc)
     * @see library/Zend/View/Zend_View_Interface::setScriptPath()
     */
    public function setScriptPath($path)
    {
        $this->_twigEnv->getLoader()->setPaths($path);
    }

    /**
     * (non-PHPdoc)
     * @see library/Zend/View/Zend_View_Interface::getScriptPaths()
     */
    public function getScriptPaths()
    {
        return $this->_twigEnv->getLoader()->getPaths();
    }

    /**
     * (non-PHPdoc)
     * @see library/Zend/View/Zend_View_Interface::setBasePath()
     */
    public function setBasePath($path, $classPrefix = 'Zend_View')
    {
        $this->setScriptPath($path);
    }

    /**
     * (non-PHPdoc)
     * @see library/Zend/View/Zend_View_Interface::addBasePath()
     */
    public function addBasePath($path, $classPrefix = 'Zend_View')
    {
        $this->setScriptPath($path);
    }

    /**
     * (non-PHPdoc)
     * @see library/Zend/View/Zend_View_Interface::__set()
     */
    public function __set($key, $val)
    {
        $this->_variablesMap[$key] = $value;
    }

    /**
     * (non-PHPdoc)
     * @see library/Zend/View/Zend_View_Interface::__isset()
     */
    public function __isset($key)
    {
        return isset($this->_variablesMap[$key]);
    }

    /**
     * (non-PHPdoc)
     * @see library/Zend/View/Zend_View_Interface::__unset()
     */
    public function __unset($key)
    {
        unset($this->_variablesMap[$key]);
    }

    /**
     * (non-PHPdoc)
     * @see library/Zend/View/Zend_View_Interface::assign()
     */
    public function assign($key, $value = NULL)
    {
        if (!is_null($value)) {
             $this->_variablesMap[$key] = $value;
        } elseif (is_array($key)) {
            $this->_variablesMap = array_merge($this->_variablesMap, $key);
        }
    }

    /**
     * (non-PHPdoc)
     * @see library/Zend/View/Zend_View_Interface::clearVars()
     */
    public function clearVars()
    {
         $this->_variablesMap = array();
    }

    /**
     * (non-PHPdoc)
     * @see library/Zend/View/Zend_View_Interface::render()
     */
    public function render($name)
    {
        $template = $this->_twigEnv->loadTemplate($name);

        return $template->render($this->_variablesMap);
    }
}

This class will serve us to inject Twig in the Zend Application context, as a Zend_View_Interface implementation. Now the next step will be override the Zend_View instance on runtime. And this can be done by creating a class that inherits of Zend_Application_Resource_ResourceAbstract.

<?php

// File located at "library/Twig/Zend/View/Twig/Twig.php"
require_once 'Zend/Application/Resource/ResourceAbstract.php';
require_once 'Twig/Zend/View/Twig/TwigImplementation.php';

defined('DS')
    || define('DS', DIRECTORY_SEPARATOR);

class Twig_Zend_View_Twig_Twig
    extends Zend_Application_Resource_ResourceAbstract
{
    /**
     * The Zend_View_TwigImplementation instance
     * @var Zend_View_TwigImplementation
     */
    protected $_view;

    /**
     * (non-PHPdoc)
     * @see library/Zend/Application/Resource/Zend_Application_Resource_Resource::init()
     */
    public function init()
    {
        return $this->_getView();
    }

    /**
     * Returns the Zend_View_TwigImplementationInstance. If not initialized,
     * initializes the instance
     */
    protected function _getView()
    {
        if (null === $this->_view) {
            $options = $this->getOptions();
            $view = new Twig_Zend_View_Twig_TwigImplementation(APPLICATION_PATH . DS . 'views' . DS . 'scripts', $options);
            Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer')
                ->setView($view)
                ->setViewSuffix('twig');

            $this->_view = $view;
        }

        return $this -&gt; _view;
    }
}

By the call to the setView method of the viewRenderer helper, the Zend_View instance will be overriden by our implementation, previously instanciated. Also we notify to the helper, about our view suffix: .twig. Then just need to modify our application.ini to begin using the engine.

; ...
; Twig config
pluginPaths.Twig_Zend_View_Twig_ = "Twig/Zend/View/Twig/"
resources.twig.cache = APPLICATION_PATH "/../cache/twig"

Now rename our views, from index.phtml to index.twig

<!-- views/scripts/index/index.twig -->
<h1>List of available sections</h1>
<ul>
{% for section in sections %}
<li>{{ section }}</li>
{% endfor %}
</ul>

And to finish this entry, I leave available all the code written in this post, at my Github account.

Zend Framework, Zend_View y Twig

Introducción a los motores de plantillas en PHP

Para contextualizar esta entrada voy a hablar un poco sobre los motores de plantillas. Los motores de plantillas son librerías que permiten abstraer todas las vistas de una aplicación buscando la máxima eficiencia y la mejor reusabilidad de código. Permiten por ejemplo, si tenemos listados de productos en diferentes categorizaciones, definir una UI común para las categorías qué lo precisen. Esto dota a la aplicación de mucha flexibilidad, y le da sentido a la V de MVC. Todo ello gracias a el control sobre la salida que proporciona PHP (cosa qué, por ejemplo, era imposible con el antiguo ASP).

En PHP existen multitud de motores de plantillas, si realizamos una búsqueda en google lo podremos comprobar. Pero básicamente, el standard de facto hasta ahora era Smarty. Sí, sí. He dicho era. Puesto que a fecha de hoy han salido otros motores de plantillas que, bajo mi punto de vista, han puesto en entredicho que Smarty sea el motor de plantillas de PHP por excelencia. Uno de ellos, y voy a mencionar el objeto de esta entrada, es Twig.

Smarty 3 vs Twig

Porqué no usar Smarty 3

Todo sea dicho. Smarty és una gran librería para la separación de la vista con la lógica de la aplicación. Cumple su función y la cumple con creces. Sin embargo, su segunda versión quedó desfasada con la salida de PHP 5 y su nuevo modelo de gestión de objetos. Es por ello, que hace poco que ha salido Smarty 3. Entre las nuevas características de Smarty 3, sumadas a las que se mantienen de versión Smarty 2, podemos destacar:

  • Reescrito para PHP5
  • Un analizador real para su sintaxis (su anterior versión no tenia)
  • Herencia de plantillas
  • Autoescaping de variables
  • Definición de funciones en contexto de plantilla

Y estas son solo por mencionar algunas, pues hay más. Quizá ahora la pregunta sea, y qué tiene de malo? Al parecer todo lo dicho hasta ahora tiene buena pinta. Pero y la performance? Cómo va afectar al rendimiento de nuestra aplicación la inclusión de uno u otro motor? Pues la verdad que para responder a esta pregunta recomiendo la lectura de un artículo de Fabien Potencier, dónde establece una comparativa de motores de plantillas en PHP, y qué cada uno seque sus propias conclusiones. Al fin y al cabo, los proyectos los acaba conociendo uno mismo y uno mismo sabe qué conviene según el caso. Yo me quedo con Twig.

Por qué sí usar Twig

En primer lugar, y cómo ya he mencionado antes, por motivos de rendimiento. Tal y cómo demuestra Fabien Potencier en su artículo hay diferencia de rendimiento entre los dos motores. Pero más allá de eso, la decisión a decantarse por este componente puede implicar otros aspectos del mismo. Con lo cuál, lo que viene a continuación es más a nivel de mi propia opinión (no lo declaro cómo estándar industrial).

En segundo lugar, Este motor tiene ciertas carácterísticas ya desarrolladas qué no vienen de serie en Smarty. Una de ellas y tengo que decir que personalmente me gusta mucho, es el modo Sandbox. El modo Sandbox, permite restringir qué características se le permite usar al editor de las plantillas.. Me refiero a, si yo no quiero que el web designer pueda usar la estructura if, mediante el modo Sandbox de Twig no la va a poder usar. Y eso con cualquier carácteristica del motor, incluso se pueden restringir nuestras propias clases de modelo. Impresionante.

Por tercer y último lugar, me gusta más el modelo de extensibilidad de Twig qué el de Smarty. Considero qué es más fácil de añadir funcionalidades a Twig. Smarty, en su versión 3, aún trae consigo el modelo de plugins de Smarty 2 y sinceramente, con PHP 5.3 actualmente en alza, hecho en falta un modelo de extensibilidad más orientado a objetos.

Zend_View y su agnosticismo

Digo agnosticismo puesto qué es una de las virtudes de este componente. Zend_View es agnóstico en cuánto a template engine se refiere, a través de la interface Zend_View_Interface, siendo PHP propiamente el template engine por defecto. Y así será cómo en este artículo voy a ilustrar cómo implementar Twig cómo extensión de Zend_View y sobreescribir la instancia por defecto de Zend_View por nuestra implementación a través de la (ya conocida :) clase abstracta Zend_Application_Resource_ResourceAbstract.


// File located at "library/Twig/Zend/View/Twig/TwigImplementation.php
require_once 'Zend/View/Interface.php';
require_once 'Twig/Autoloader.php';

class Twig_Zend_View_Twig_TwigImplementation
    implements Zend_View_Interface
{
	/**
	 * The twig loader instance
	 * @var Twig_Environment
	 */
	protected $_twigEnv;

	/**
	 * The twig loader instance
	 * @var Twig_Loader_Interface
	 */
	protected $_twigLoader;

	/**
	 * A map of all registered variables
	 * @var array
	 */
	protected $_variablesMap = array();

	/**
	 * Class constructor
	 * @param string $templateBasePath The views base path
	 * @param array $envOptions 
	 */
	public function __construct($templateBasePath, array $envOptions = array())
	{
		$zendAutoloader = Zend_Loader_Autoloader::getInstance();
		$zendAutoloader -> pushAutoloader(array('Twig_Autoloader', 'autoload'));

		$this -> _twigLoader = new Twig_Loader_Filesystem($templateBasePath);
		$this -> _twigEnv = new Twig_Environment($this -> _twigLoader, $envOptions);
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/View/Zend_View_Interface::getEngine()
	 * @return Twig_Environment
	 */
	public function getEngine()
	{
		return $this -> _twigEnv;
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/View/Zend_View_Interface::setScriptPath()
	 */
	public function setScriptPath($path)
	{
	 	$this -> _twigEnv -> getLoader() -> setPaths($path);
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/View/Zend_View_Interface::getScriptPaths()
	 */
	public function getScriptPaths()
	{
		return $this -> _twigEnv -> getLoader() -> getPaths();
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/View/Zend_View_Interface::setBasePath()
	 */
	public function setBasePath($path, $classPrefix = 'Zend_View')
	{
		$this -> setScriptPath($path);
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/View/Zend_View_Interface::addBasePath()
	 */
	public function addBasePath($path, $classPrefix = 'Zend_View')
	{
		$this -> setScriptPath($path); 
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/View/Zend_View_Interface::__set()
	 */
	public function __set($key, $val)
	{
		$this -> _variablesMap[$key] = $value;
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/View/Zend_View_Interface::__isset()
	 */
	public function __isset($key)
	{
		return isset($this -> _variablesMap[$key]);
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/View/Zend_View_Interface::__unset()
	 */
	public function __unset($key)
	{
		unset($this -> _variablesMap[$key]);
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/View/Zend_View_Interface::assign()
	 */
	public function assign($key, $value = NULL)
	{
		if (!is_null($value)) {
			$this -> _variablesMap[$key] = $value;
		} elseif (is_array($key)) {
			$this -> _variablesMap = array_merge($this -> _variablesMap, $key);
		}
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/View/Zend_View_Interface::clearVars()
	 */
	public function clearVars()
	{
		$this -> _variablesMap = array();
	}

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/View/Zend_View_Interface::render()
	 */
	public function render($name)
	{
		$template = $this -> _twigEnv -> loadTemplate($name);
		return $template -> render($this -> _variablesMap);
	}
}

Está clase nos va a servir para introducir Twig en el contexto de Zend Application, cómo implementación de Zend_View_Interface. Ahora el siguiente paso es sobreescribir la instancia de Zend_View en tiempo de ejecución. Y eso lo podemos llevar a cabo creando una clase que herede de Zend_Application_Resource_ResourceAbstract.


// File located at "library/Twig/Zend/View/Twig/Twig.php"
require_once 'Zend/Application/Resource/ResourceAbstract.php';
require_once 'Twig/Zend/View/Twig/TwigImplementation.php';

defined('DS')
    || define('DS', DIRECTORY_SEPARATOR);

class Twig_Zend_View_Twig_Twig
    extends Zend_Application_Resource_ResourceAbstract
{
	/**
	 * The Zend_View_TwigImplementation instance
	 * @var Zend_View_TwigImplementation
	 */
	protected $_view;

	/**
	 * (non-PHPdoc)
	 * @see library/Zend/Application/Resource/Zend_Application_Resource_Resource::init()
	 */
	public function init()
	{
		return $this -> _getView();
	}

	/**
	 * Returns the Zend_View_TwigImplementationInstance. If not initialized,
	 * initializes the instance
	 */
	protected function _getView()
	{
		if (null === $this -> _view)
		{
			$options = $this -> getOptions();
			$view = new Twig_Zend_View_Twig_TwigImplementation(APPLICATION_PATH . DS . 'views' . DS . 'scripts', $options);
			Zend_Controller_Action_HelperBroker::getStaticHelper('viewRenderer')
                -> setView($view)
                -> setViewSuffix('twig');

			$this -> _view = $view;
		}

		return $this -> _view;
	}
}

A través de la llamada a setView del helper viewRenderer se va a sobreescribir la instancia de Zend_View por nuestra implementación, previamente instanciada y además también le notificamos qué nuestras vistas tendrán la extensión .twig. Luego sólo falta modificar nuestro application.ini para poder empezar a usar el motor.

; ...
; Twig config
pluginPaths.Twig_Zend_View_Twig_ = "Twig/Zend/View/Twig/"
resources.twig.cache = APPLICATION_PATH "/../cache/twig"

Ya ahora renombramos nuestras vistas, de index.phtml a index.twig

<!-- views/scripts/index/index.twig -->
<h1>List of available sections</h1>
<ul>
    {% for section in sections %}
    <li>{{ section }}</li>
    {% endfor %}
</ul>

Y ya para finalizar este artículo, dejo disponible todo el código de este artículo, en mi cuenta de Github

Zend Framework and Doctrine 2 CLI integration

CLI Environtment introduction

The CLI environtments are environtments executed directly on the OS shell. It’s name, CLI, is an acronym of Command Line Interface and are used for specific tasks execution. For this case, both tools have their own CLI tool: the Zend Tool by Zend Framework and the Doctrine CLI by Doctrine 2. Both CLI tools, are intended for code generation and for agilize web application maintenance tasks. For example: the creation of a Zend Framework application with its specific layout, the generation of the different domain model classes of Zend_Db by the Zend Tool or the Doctrine 2 entities, repositories and proxies generation.

Firsts steps. Understanding Zend Tool internals

Zend Tool, consists of several components to make easier the task integration on its context. On one hand there is the Zend_Tool_Framework family components which broadly provides the integration of web application independent tasks. For example, If we have to design a task that provides an entrypoint to the system cron without using web application resources, probably the best way will be using the Zend_Tool_Framework. However, if the task, we have to design, depends in some point on a resource of the web application, sure It will need to know about web application aspects. And this can be done with the Zend_Tool_Project family components (wich is the component that I will use, right here and right now).

Once the tasks have been designed and implemented, Zend Tool must be noticed about of its presence by the .zf.ini file, located on the user home.

Music, maestro!

On this post I’m going to describe, the creation of two tasks that will be linked to a Zend Framework web application with Doctrine 2 integrated as O/RM. This two tasks, will be responsible primarily to configure the Zend Application to be able to execute Doctrine 2 without problems (I have explained how on my previous post, you can check it a here) and to generate several useful files for its execution (proxies and repositories).

Analyzing a bit the Zend Tool Project and the Doctrine 2 CLI architecture we are going to write an abstract class that will be useful to prepare all the Doctrine 2 environment and it can be extended to create diferent provider tasks.

<?php
/**
 * File located at /library/Doctrine/Zend/Tool/Project/Provider/Abstract.php
 */

require_once 'Zend/Tool/Project/Provider/Abstract.php';
require_once 'Doctrine/Common/ClassLoader.php';

abstract class Doctrine_Zend_Tool_Project_Provider_Abstract
    extends Zend_Tool_Project_Provider_Abstract
{
    /**
     * A class property to hold the Zend_Tool_Project_Profile instance.
     * @var Zend_Tool_Project_Profile
     */
    protected $_profile;
    
    /**
     * The application.ini config file
     * @var Zend_Config_Ini
     */
    protected $_config;
    
    /**
     * The path to the application.ini
     * @var string
     */
    protected $_appConfigFilePath;
    
    /**
     * The path to the application directory
     * @var string
     */
    protected $_appPath;
    
    /**
     * The target section of the application.ini we want to work on
     * @var string
     */
    protected $_sectionName;
    
    /**
     * The Doctrine CLI instance, ready to execute tasks
     * @var Symfony\Component\Console\Application
     */
    protected $_doctrineCli = NULL;
    
    /**
     * Prepares Doctrine 2 for task performing, with the application.ini stuff
     * @return \Symfony\Component\Console\Application $cli
     */
    protected function _prepare($sectionName = 'production')
    {
    	if (is_null($this -> _doctrineCli)) {
    		// Load the project profile (.zfproject.xml)
	        $this -> _profile = $this -> _loadProfile(self::NO_PROFILE_THROW_EXCEPTION);
	        $appConfigFileResource = $this -> _profile -> search('applicationConfigFile');
	                
	        if ($appConfigFileResource == FALSE) {
	            throw new Zend_Tool_Project_Exception('A project with an application config file is required to use this provider.');
	        }
	        
	        $this -> _appConfigFilePath = $appConfigFileResource -> getPath();
	        
	        $appDirResource = $this -> _profile -> search('applicationDirectory');
	        $this -> _appPath = $appDirResource -> getPath();
	        
	        // This define it's important in order to work with the application.ini, since it's paths are configured
	        // with the APPLICATION_PATH constants. So define it just before instance the Zend_Config_Ini.
	        define('APPLICATION_PATH', $this -> _appPath);
	        $this -> _config = new Zend_Config_Ini($this -> _appConfigFilePath);
	        
	        $this -> _sectionName = $sectionName;
	        
	        if (!isset($this -> _config -> {$this -> _sectionName})) {
	            throw new Zend_Tool_Project_Exception('The config does not have a ' . $this -> _sectionName . ' section.');
	        }
	        
	        /**
	         * Maybe at this point doctrine isn't configured on the application.ini, do all this stuff only if
	         * configured
	         */
	        if (isset($this -> _config -> {$this -> _sectionName} -> resources -> doctrine)) {
		        $doctrineConsoleHelperSet = $this -> _getDoctrineConsoleHelperSet();
		        $this -> _doctrineCli = new \Symfony\Component\Console\Application('Doctrine Command Line Interface', Doctrine\ORM\Version::VERSION);
				$this -> _doctrineCli -> setCatchExceptions(TRUE);
				$this -> _doctrineCli -> setHelperSet($doctrineConsoleHelperSet);
				$this -> _doctrineCli -> addCommands(array(
				    new \Doctrine\DBAL\Tools\Console\Command\RunSqlCommand(),
				    new \Doctrine\DBAL\Tools\Console\Command\ImportCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\ClearCache\MetadataCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\ClearCache\ResultCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\ClearCache\QueryCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\SchemaTool\UpdateCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\SchemaTool\DropCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\EnsureProductionSettingsCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\ConvertDoctrine1SchemaCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\GenerateRepositoriesCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\GenerateEntitiesCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\GenerateProxiesCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\ConvertMappingCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\RunDqlCommand(),
				    new \Doctrine\ORM\Tools\Console\Command\ValidateSchemaCommand()
				));

				return $this -> _doctrineCli;
			}
		}
    }
    
    /**
     * Builds the Doctrine 2 Entity Manager object, and the helper set, needed by the
     * Doctrine 2 Console.
     * @return \Symfony\Component\Console\Helper\HelperSet $helperSet
     */
    private function _getDoctrineConsoleHelperSet()
    {
    	$classLoader = new \Doctrine\Common\ClassLoader('Doctrine');
		$classLoader->register();
		
		$classLoader = new \Doctrine\Common\ClassLoader('Symfony', 'Doctrine');
		$classLoader->register();
		
    	$classLoader = new \Doctrine\Common\ClassLoader('Application\Models', dirname(APPLICATION_PATH));
		$classLoader->register();
		
		$classLoader = new \Doctrine\Common\ClassLoader('Application\Proxies', dirname(APPLICATION_PATH));
		$classLoader->register();
		
		$classLoader = new \Doctrine\Common\ClassLoader('Application\Repositories', dirname(APPLICATION_PATH));
        $classLoader->register();
		
		$entityManager = $this -> _buildDoctrineEntityManager();
		return new \Symfony\Component\Console\Helper\HelperSet(array(
            'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($entityManager->getConnection()),
            'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($entityManager) 
        ));
    }
    
    /**
     * Builds the EntityManager for Doctrine 2
     * @return \Doctrine\ORM\EntityManager
     */
    private function _buildDoctrineEntityManager()
    {
    	$config = new \Doctrine\ORM\Configuration();
		$cache = new \Doctrine\Common\Cache\ArrayCache();
		$config -> setMetadataCacheImpl($cache);
		$config -> setMetadataDriverImpl(\Doctrine\ORM\Mapping\Driver\AnnotationDriver::create(array(
            $this -> _config -> {$this -> _sectionName} -> resources -> doctrine -> entitiesPath
		)));
		$config -> setProxyDir($this -> _config -> {$this -> _sectionName} -> resources -> doctrine -> proxiesPath);
		$config -> setProxyNamespace('Application\Proxies');
		$config -> setAutoGenerateProxyClasses(TRUE);
		
		return \Doctrine\ORM\EntityManager::create($this -> _config -> {$this -> _sectionName} -> resources -> doctrine -> connection -> toArray(), $config);
    }
    
    /**
     * This method will be responsible for running any task on the Doctrine 2 CLI
     * Usage:
     *      
     *     $this -> _run(array(
     *         'command' => 'orm:generate-entities',
     *         'dest-path' => '/application/models'
     *     ), 'production');
     *     
     * @param array $arguments The task arguments
     * @param string $sectionName
     * @throws Zend_Tool_Project_Exception
     */
    protected function _run(array $arguments, $sectionName = 'production')
    {
    	if (is_null($this -> _doctrineCli)) {
            $doctrineCli = $this -> _prepare($sectionName);
    	} else {
    		$doctrineCli = $this -> _doctrineCli;
    	}
    	
        if (!is_null($doctrineCli)) {
            $consoleOutput = new \Symfony\Component\Console\Output\ConsoleOutput();
            $consoleOutput -> setDecorated(TRUE);
            $doctrineCli -> run(new \Symfony\Component\Console\Input\ArrayInput($arguments), $consoleOutput);
        } else {
        	throw new Zend_Tool_Project_Exception('Doctrine has not been configured on this project. Run first, the task configure on the doctrine-config provider.');
        }
    }
}

This abstract class will allow us to execute Doctrine 2 CLI tasks by the options defined on the application.ini file. So then, we define the following tasks

Provider to configure Doctrine 2

<?php

/**
 * File 'Doctrine/Zend/Tool/Project/Provider/DoctrineConfigProvider.php
 */
require_once 'Doctrine/Zend/Tool/Project/Provider/Abstract.php';

class Doctrine_Zend_Tool_Project_Provider_DoctrineConfigProvider
    extends Doctrine_Zend_Tool_Project_Provider_Abstract
{
	/**
	 * This task configures Doctrine environtment on the application.ini.
	 * Usage:
	 *     
	 *     > zf config doctrine-provider \
	 *       'connection[driver]=pdo_sqlite&connection[path]=APPLICATION_PATH "/../data/db/database-dev"' \
	 *       development
	 * 
	 * The available key configurations are as follows:
	 *     · connection: an array with all the key configs needed to configure a doctrine dbal connection
	 *       see http://www.doctrine-project.org/projects/dbal/2.0/docs/reference/configuration/en#configuration for more info
	 *     · entitiesPath: The path where entities will reside
	 *     · proxiesPath: The path where proxies will reside
	 *     · repositoriesPath: The path where repositories will reside
	 *     · cacheClass: The class used to act as a object cache (One from \Doctrine\Common\Cache)
	 * @param string $connectionQueryString
	 * @param string $sectionName
	 * @throws Zend_Tool_Project_Exception
	 */
	public function config($connectionQueryString, $sectionName = 'production')
	{
		$this -> _prepare($sectionName);
		parse_str($connectionQueryString, $options);
		if (isset($options['connection']) && isset($options['connection']['driver']) && method_exists($this, "_{$options['connection']['driver']}")) {
			$method = "_{$options['connection']['driver']}";
			$this -> $method($options, $sectionName);
		} else {
			throw new Zend_Tool_Project_Exception('Unsupported driver (' . $options['connection']['driver'] . ')');
		}
	}
	
	/**
	 * A method to configure the connection to the database. To implement more connections,
	 * define for exemple "_pdo_mysql" or "_pdo_pgsql" with the same signature as this method.
	 * This method only validates the options of the connection. The real implementation is on
	 * the "_setConfig" method.
	 * @param array $options The options defined
	 * @param unknown_type $sectionName
	 * @throws Zend_Tool_Project_Exception
	 */
	protected function _pdo_sqlite(array $options, $sectionName = 'production')
	{
		// Build the options array
		if (!isset($options['connection']['path']) && !isset($options['connection']['memory'])) {
            throw new Zend_Tool_Project_Exception('Malformed connection string');
        }
        
        $this -> _setConfig($options, $sectionName);
	}
	
	/**
	 * This method serializes all the config options from Doctrine 2 CLI to the
	 * application.ini
	 * @param array $options The defined options
	 * @param string $sectionName
	 */
	protected function _setConfig(array $options, $sectionName = 'production')
	{
		$doctrineItem = array(
            'resources' => array(
                'doctrine' => array()
            )
        );
        
        $doctrineItem['resources']['doctrine']['entitiesPath'] = isset($options['entitiesPath']) ? $options['entitiesPath'] : 'APPLICATION_PATH "/models"';
        $doctrineItem['resources']['doctrine']['proxiesPath'] = isset($options['proxiesPath']) ? $options['proxiesPath'] : 'APPLICATION_PATH "/proxies"';
        $doctrineItem['resources']['doctrine']['repositoriesPath'] = isset($options['repositoriesPath']) ? $options['repositoriesPath'] : 'APPLICATION_PATH "/repositories"';
        $doctrineItem['resources']['doctrine']['cacheClass'] = isset($options['cacheClass']) ? $options['cacheClass'] : 'Doctrine\Common\Cache\ApcCache';
        $doctrineItem['resources']['doctrine']['connection'] = $options['connection'];
        $appConfigFileResource = $this -> _profile -> search('applicationConfigFile');
        $appConfigFileResource -> addItem($doctrineItem, $sectionName, NULL);
        $appConfigFileResource -> create();
        $this -> _registry -> getResponse() -> appendContent("A new Doctrine 2 configuration for the section $sectionName, has been stablished.");
	}
}

Entities, Proxies and Repositories generation Provider

<?php

/**
 * File /Doctrine/Zend/Tool/Project/Provider/DoctrineOrmProvider.php
 */
require_once 'Doctrine/Zend/Tool/Project/Provider/Abstract.php';

class Doctrine_Zend_Tool_Project_Provider_DoctrineOrmProvider
    extends Doctrine_Zend_Tool_Project_Provider_Abstract
{
	/**
	 * A task to generate the entity repositories on a defined path
	 * @param string $destPath
	 * @param string $sectionName
	 */
	public function generateRepositories($destPath, $sectionName = 'production')
	{
		$this -> _run(array(
            'command' => 'orm:generate-repositories',
            'dest-path' => $destPath
		));
	}
	
	/**
	 * A task to generate proxies on a defined path
	 * @param string $destPath
	 * @param string $sectionName
	 */
	public function generateProxies($destPath, $sectionName = 'production')
	{
		$this -> _run(array(
            'command' => 'orm:generate-proxies',
            'dest-path' => $destPath
		));
	}
	
	/**
	 * A task to generate entities on a defined path
	 * @param string $destPath
	 * @param string $sectionName
	 */
    public function generateEntities($destPath, $sectionName = 'production')
    {
        $this -> _run(array(
            'command' => 'orm:generate-entities',
            'dest-path' => $destPath
        ));
    }
}

Concluding

The only thing to do from here, is to notice the Zend Tool about the presence of this tasks in our CLI environment. For this, as I said before, we’re going to use the “.zf.ini” file, located on our user’s home directory. We edit this file and update the include_path to point to the library directory of our project and also add the recently created classes.

php.include_path = ".:/path/to/my/zend/framework/project/library:/usr/local/zend/share/pear"
basicloader.classes.0 = "Doctrine_Zend_Tool_Project_Provider_DoctrineConfigProvider"
basicloader.classes.1 = "Doctrine_Zend_Tool_Project_Provider_DoctrineOrmProvider"

With this we should be able to execute Doctrine 2 CLI tasks, by the Zend Tool.

By and for the community … here my two cents

I’ve decided to release the code exposed here on my posts. For this, I’ll use my Github account to put, in a repositories way, all the code. At the moment there is only available the first post code about Zend Framework and Doctrine 2. For those of you interested, feel free to download and use it. I hope to contribute my bit to the community.