Zend Framework and Doctrine 2

The perfect marriage?

That Zend Framework is one of the best positioned framework for developing PHP web applications, it’s a fact. It can be used as a component stack, or as a full MVC stack taking advantage of the rest of the components.

The tendency, it’s clear. ZF follows the line defined by Ruby on Rails and the rest of the frameworks, in the way that all (or almost all) the conventions already defined by others, prevail on this. And this implies a better and easier learning step by all the people than want to get started on. But it’s not gold, all that glitters (typical Spanish proverb). At least, from my point of view, I quite like the layer that intereacts with all the domain model (the Zend_Db components family), beacause I think that It hasn’t still became to an O/RM properly. Despite of implement a design pattern based on Table Data Gateway and on Row Data Gateway.

For the domain model layer interaction, I try to use the Doctrine 2 project (I haven’t use it in all of my projects). Doctrine 2 is an O/RM, also strongly based on Data Mapper. Features like database introspection for full schema generation, code annotation based models (@Anotations == POPOs — Plain Old PHP Objects), or it’s design based on Data Mapper (and on the mythic Hibernate java project) makes it a good option. But, going to the point, now it comes the great question …

What integration possibilities there are between the two projects?

Actually, all. Both are very powerful projects, totally integrable between them. And on this post I’ll describe minutely all the process. This post pretend to be the first of a series of blog posts related to the development under Zend Framework. So, there will be more.

Understanding Zend Framework

To begin, Zend Framework has a default directory layout. This is a directory where all the application resources are saved, a directory where to store all the external libraries, another as a public directory and another as a store for all the test for our application.

Following the Zend Framework manual, it’s really easy to create a new project. So I suggest to follow the manual if someone doesn’t now how to get started on the ZF development.

An interesting feature about Zend Framework is that allows to the developer to interact on the Bootstrap phase, to inject all the needed components. And this is allowed in way that I consider so so elegant: by the Zend_Application_Resource_ResourceAbstract component. This class will allow us to define an interface between ZF and Doctrine 2, in a way that It will allow us to integrate all the domain model generated by Doctrine 2 on the Zend context.

The commitment

/**
 * File located in library/Doctrine/Zend/Doctrine.php
 */
require_once 'Zend/Application/Resource/ResourceAbstract.php';
require_once 'Doctrine/Common/ClassLoader.php';

class Doctrine_Zend_Doctrine
    extends Zend_Application_Resource_ResourceAbstract
{
    public function init()
    {
        $options = $this -> getOptions();

        // Doctrine (use include_path)
        $classLoader = new \Doctrine\Common\ClassLoader('Doctrine');
        $classLoader -> register();

        // Entities
        $classLoader = new \Doctrine\Common\ClassLoader(
            'Application\Models',
            dirname(APPLICATION_PATH) . DIRECTORY_SEPARATOR . 'library'
        );
        $classLoader -> register();

        // Proxies
        $classLoader = new \Doctrine\Common\ClassLoader(
            'Application\Models\Proxies',
            dirname(APPLICATION_PATH) . DIRECTORY_SEPARATOR . 'library'
        );
        $classLoader -> register();

        // Now configure doctrine
        if ('development' == APPLICATION_ENV) {
            $cache = new \Doctrine\Common\Cache\ArrayCache();
        } else {
            $cache = new \Doctrine\Common\Cache\ApcCache();
        }

        $config = new Configuration();
        $config -> setMetadataCacheImpl($cache);
        $driverImpl = $config -> newDefaultAnnotationDriver($options['entitiesPath']);
        $config -> setMetadataDriverImpl($driverImpl);
        $config -> setQueryCacheImpl($cache);
        $config -> setProxyDir($options['proxiesPath']);
        $config -> setProxyNamespace('Application\Models\Proxies');
        $config -> setAutoGenerateProxyClasses(('development' == APPLICATION_ENV));
        $em = EntityManager::create(
            $this -> _buildConnectionOptions($options),
            $config
        );

        // Once we have the EntityManager ready, add it to the registry
        Zend_Registry::set('em', $em);

        // end
        return $em;
    }

    /**
     * A method to build the connection options, for a Doctrine
     * EntityManager/Connection. Sure, we can find a more elegant solution to build
     * the connection options. A builder class could be applied. Sure you can with
     * some refactor!
     * TODO: refactor to build some other, more elegant, solution to build the conn
     * ection object.
     * @param Array $options The options array defined on the application.ini file
     * @return Array
     */
    protected function _buildConnectionOptions($options)
    {
        $connectionSpec = array(
            'pdo_sqlite' => array('user', 'password', 'path', 'memory'),
            'pdo_mysql'  => array(
                'user', 'password', 'host', 'port', 'dbname', 'unix_socket'
            ),
            'pdo_pgsql'  => array('user', 'password', 'host', 'port', 'dbname'),
            'pdo_oci'    => array(
                'user', 'password', 'host', 'port', 'dbname', 'charset'
            )
        );

        $connection = array(
            'driver' => $options['driver']
        );

        foreach ($connectionSpec[$options['driver']] as $driverOption) {
            if (isset($options[$driverOption]) && !is_null($driverOption)) {
                $connection[$driverOption] = $options[$driverOption];
            }
        }

        if (isset($options['driverOptions'])
            && !is_null($options['driverOptions'])) {
            $connection['driverOptions'] = $options['driverOptions'];
        }

        return $connection;
    }
}

With this on our include_path, It cannot be anymoure doubts … We are going to ask for the hand

Asking for the hand

With this class on the ZF execution context, Doctrine 2 can be configured by the application.ini file.

; ...
[production]
; Doctrine 2 Config
pluginPaths.Doctrine_Zend_ = "Doctrine/Zend/"
resources.doctrine.entitiesPath = APPLICATION_PATH "/../library/models"
resources.doctrine.proxiesPath = APPLICATION_PATH "/../library/models/proxies"
resources.doctrine.driver = pdo_sqlite
resources.doctrine.path = APPLICATION_PATH "/../data/db/database-dev.db"

The next step, would be to generate all the entities and the proxies needed. This can be done by the Doctrine 2 console tool (next post subject: integration between the Doctrine 2 console tool and the ZF tool). And finally …

The Wedding

Words are no needed, the code speaks by itself.

<?php

class SectionController extends Zend_Controller_Action
{
    public function init()
    {
        /* Initialize action controller here */
    }

    public function indexAction()
    {
        $sections = Zend_Registry::get('em')
            -> createQuery('select s from \Application\Models\Section s')
            -> getResult();

        $this -> view -> sections = $sections;
    }
}

And only remains to be seen flying through the air of the rice grains and the typical wedding song. Like I said on another previous post: POETRY! PURE POETRY!

PD: For those who are interested, the code-example of this entry it’s available and can be downloaded from my Github account. Enjoy it! :D

PD 2: I must apologize to the people who has downloaded the example code to view and test it. I must admit that there were some errors that didn’t allow its correct execution. I have already fixed it, so now it must work without problems. Greetings and apologies for any inconvenience.

Integración de Zend Framework con Doctrine 2 CLI

Introducción a los entronos de CLI

Los entornos CLI son entornos ejecutados directamente en el shell del sistema operativo. Su nombre CLI es acrónimo de Command Line Interface (Interfaz de Linea de Comandos) y son usados para la ejecución de tareas muy especificas. Para el caso que nos ocupa, ambas herramientas disponen de sus respectivas herramientas CLI: la Zend Tool por parte de Zend Framework y la Doctrine CLI por parte de Doctrine 2. Ambas herramientas CLI, están pensadas para la generación de código y para aligerar tareas de mantenimiento de aplicaciones web. Por ejemplo: la creación de una aplicación web Zend Framework con su layout especifico, la creación de las distintas clases del modelo de dominio de Zend_Db por parte de Zend Tool o la creación de Entities, Proxies o Repositories en Doctrine 2.

Primeros pasos. Entendiendo las entrañas de Zend Tool

Zend Tool, consta de varios componentes que facilitan la integración de tareas en su contexto. Por una parte está la familia de componentes Zend_Tool_Framework que, a grandes rasgos, facilita la integración de tareas independientes de aplicaciones web. Por ejemplo si tuviéramos que diseñar una tarea que sirviera cómo interface al cron del sistema (por poner un ejemplo) sin que este debiera usar recursos de la aplicación web, muy probablemente la mejor manera sea usando Zend_Tool_Framework. En cambio si la tarea que debemos diseñar depende en algún momento de algún recurso de la aplicación web, seguro necesitará conocer determinados aspectos de la misma. Y esto se puede conseguir a través de la familia de componentes de Zend_Tool_Project (qué es el componente que vamos a usar aquí y ahora).

Una vez ya con las tareas diseñadas e implementadas, se le debe notificar a la Zend Tool de la existencia de las mismas a través del archivo .zf.ini situado dentro del espacio del usuario que ejecuta el shell.

Música, maestro!

En este artículo voy a detallar, la creación de dos tareas que se van a vincular a una aplicación web Zend Framework con Doctrine 2 integrado cómo O/RM. Estas dos tareas, se van a encargar principalmente de configurar Zend Application para que pueda ejecutar Doctrine 2 sin problemas (el cómo lo expliqué en mi anterior artículo, puedes verlo aquí) y de generar diferentes archivos útiles para su ejecución (proxies y repositories).

Analizando un poco qué arquitectura de tanto Zend Tool Project cómo de Doctrine 2 CLI vamos a plantear una clase genérica que sirva para preparar todo el entorno de Doctrine 2 y se pueda extender para crear diferentes providers de tareas.

<?php
/**
 * Archivo ubicado en /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.');
        }
    }
}

Esta clase abstracta nos va a permitir ejecutar tareas del CLI de Doctrine a través de las opciones definidas en al archivo application.ini. Así de este modo definimos las siguientes tareas

Provider para configurar Doctrine 2

<?php

/**
 * Archivo '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.");
	}
}

Provider para la generación de Entities, Proxies y Repositories

<?php

/**
 * Archivo /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
        ));
    }
}

Concluyendo

Llegados hasta aquí sólo falta notificar a Zend Tool de la existencia de estas tareas en nuestro entorno CLI. Para ello, cómo he dicho antes vamos a usar el archivo “.zf.ini” situado en nuestra carpeta de usuario del sistema. Editamos dicho archivo y le modificamos el include_path para que apunte también al directorio library de nuestro proyecto y además le añadimos las clases que acabamos de crear.

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"

Con esto deberíamos ser capaces de ejecutar CLI tasks de Doctrine 2, a través de la Zend Tool

Prestahop, virtudes y “desvirtudes”

Mientras acabo de preparar la siguiente entrada de ZF y Doctrine 2, he tenido el gusto de leer un interesante análisis sobre Prestashop destacando sus pros y sus contras: http://juanmacias.net/archives/805.

Zend Framework y Doctrine 2

El matrimonio perfecto?

De los qué para mí está mejor posicionado dentro de la escena de frameworks disponibles para PHP, es sin dudar Zend Framework. Puede usarse tanto cómo stack de componentes, cómo stack MVC completo aprovechando las bondades del resto de componentes.

La tendencia, clara. Sigue la linea marcada por Ruby on Rails y por ende por el resto de frameworks. De modo qué la mayor parte de las convenciones ya definidas por otros, prevalecen en este. Y esto implica un mejor y más fácil aprendizaje por parte de todos aquellos qué quieran iniciarse. De este ORM se puede decir que implementa principalmente los patrones Table Data Gateway y en Row Data Gateway.

Para la interacción con el modelo de dominio en aplicaciones web que requieran una capa de dominio muy grande, intento usar el proyecto Doctrine 2 o algún ORM que implemente DataMapper. Doctrine 2 és un O/RM, basado en Data Mapper qué realmente promete mucho. Características cómo la introspección de bases de datos para generar schemas enteros, modelos basados en anotaciones sobre el código (@Anotations == POPOs — Plain Old PHP Objects), o el propio diseño basado en Data Mapper (y en el mítico proyecto Hibernate de Java) lo hacen una opción de lo más apetecible. Pero, y ya yendo al grano, ahora viene la gran pregunta …

Qué posibilidades de integración existen entre los dos proyectos?

Pues en realidad, todas. Los dos son proyectos muy potentes totalmente integrables entre sí. Y en esta entrada voy a describir el proceso minuciosamente. Este pretende ser el primero de varios artículos relacionados con el desarrollo bajo Zend Framework. Así qué, habrá más.

Entendiendo el funcionamiento de Zend Framework

Para empezar, Zend Framework posee un layout de directorios predeterminado. Esto es un directorio dónde se almacenan los recursos que necesita la aplicación, un directorio dónde almacenar librerías externas, otro cómo directorio público y otro destinado al almacén de los tests de nuestra aplicación.

Siguiendo el manual de Zend Framework, es realmente fácil crear un nuevo proyecto. Así qué recomiendo seguir el manual si alguien no sabe cómo iniciarse en el desarrollo de ZF.

Una de las características interesantes de Zend Framework es que permite al desarrollador intervenir en la fase de Bootstrap, para “inyectar” los componentes que se necesite. Y lo permite hacer de una forma qué considero muy elegante: a través del componente Zend_Application_Resource_ResourceAbstract. Esta classe nos va a permitir definir una interface entre ZF y Doctrine2, de tal modo que nos permita integrar todo el modelo de dominio generado por Doctrine 2 en el contexto de Zend.

El compromiso

/**
 * Archivo ubicado en library/Doctrine/Zend/Doctrine.php
 */
require_once 'Zend/Application/Resource/ResourceAbstract.php';
require_once 'Doctrine/Common/ClassLoader.php';

class Doctrine_Zend_Doctrine
  extends Zend_Application_Resource_ResourceAbstract
{
  public function init()
  {
    $options = $this -> getOptions();

    // Doctrine (use include_path)
    $classLoader = new \Doctrine\Common\ClassLoader('Doctrine');
    $classLoader -> register();

    // Entities
    $classLoader = new \Doctrine\Common\ClassLoader(
        'Application\Models',
        dirname(APPLICATION_PATH) . DIRECTORY_SEPARATOR . 'library'
    );
    $classLoader -> register();

    // Proxies
    $classLoader = new \Doctrine\Common\ClassLoader(
        'Application\Models\Proxies',
        dirname(APPLICATION_PATH) . DIRECTORY_SEPARATOR . 'library'
    );
    $classLoader -> register();

    // Now configure doctrine
    if ('development' == APPLICATION_ENV) {
        $cache = new \Doctrine\Common\Cache\ArrayCache();
    } else {
        $cache = new \Doctrine\Common\Cache\ApcCache();
    }

    $config = new Configuration();
    $config -> setMetadataCacheImpl($cache);
    $driverImpl = $config -> newDefaultAnnotationDriver($options['entitiesPath']);
    $config -> setMetadataDriverImpl($driverImpl);
    $config -> setQueryCacheImpl($cache);
    $config -> setProxyDir($options['proxiesPath']);
    $config -> setProxyNamespace('Application\Models\Proxies');
    $config -> setAutoGenerateProxyClasses(('development' == APPLICATION_PATH));
    $em = EntityManager::create(
        $this -> _buildConnectionOptions($options),
        $config
    );

    // Once we have the EntityManager ready, add it to the registry
    Zend_Registry::set('em', $em);

    // end
    return $em;
  }

  /**
   * A method to build the connection options, for a Doctrine
   * EntityManager/Connection. Sure, we can find a more elegant solution to build
   * the connection options. A builder class could be applied. Sure you can with
   * some refactor!
   * TODO: refactor to build some other, more elegant, solution to build the conn
   * ection object.
   * @param Array $options The options array defined on the application.ini file
   * @return Array
   */
  protected function _buildConnectionOptions($options)
  {
    $connectionSpec = array(
      'pdo_sqlite' => array('user', 'password', 'path', 'memory'),
      'pdo_mysql'  => array(
        'user', 'password', 'host', 'port', 'dbname', 'unix_socket'
      ),
      'pdo_pgsql'  => array('user', 'password', 'host', 'port', 'dbname'),
      'pdo_oci'    => array(
        'user', 'password', 'host', 'port', 'dbname', 'charset'
      )
    );

    $connection = array(
      'driver' => $options['driver']
    );

    foreach ($connectionSpec[$options['driver']] as $driverOption) {
      if (isset($options[$driverOption]) && !is_null($driverOption)) {
        $connection[$driverOption] = $options[$driverOption];
      }
    }

    if (isset($options['driverOptions'])
      && !is_null($options['driverOptions'])) {
      $connection['driverOptions'] = $options['driverOptions'];
    }

    return $connection;
  }
}

Con esto en nuestro include_path, ya no debe caber duda … vamos a pedir la mano

Pidiendo la mano

Con esa clase en el entorno de ejecución de ZF, ya se puede configurar a través del application.ini.

; ...
[production]
; Doctrine 2 Config
pluginPaths.Doctrine_Zend_ = "Doctrine/Zend/"
resources.doctrine.entitiesPath = APPLICATION_PATH "/../library/models"
resources.doctrine.proxiesPath = APPLICATION_PATH "/../library/models/proxies"
resources.doctrine.driver = pdo_sqlite
resources.doctrine.path = APPLICATION_PATH "/../data/db/database-dev.db"

El siguiente paso seria generar las entidades y los proxies necesarios. Esto se puede hacer a través de la herramienta de consola de Doctrine 2 (tema del próximo artículo: integración de las herramientas de consola de Doctrine 2 con la ZF tool). Y ya por último …

La Boda

Sobran palabras, el código habla por si sólo.

<?php

class SectionController extends Zend_Controller_Action
{
    public function init()
    {
        /* Initialize action controller here */
    }

    public function indexAction()
    {
        $sections = Zend_Registry::get('em')
            -> createQuery('select s from \Application\Models\Section s')
            -> getResult();

        $this -> view -> sections = $sections;
    }
}

Ya sólo faltaria ver volar por el aire los granos de arroz y la típica canción de boda. Cómo dije en algún post anterior: POESíA! PURA POESíA!

PD: Para quién le interese, el código-ejemplo de esta entrada se puede encontrar en mi cuenta de Github.

PD 2: Debo pedir disculpas a quién se haya molestado a descargar el código de ejemplo y a probarlo. Debo admitir qué había presentes algunos errores que impedían la correcta ejecución de la applicación de ejemplo. Ya he podido solucionarlos, con lo qué ahora debería poder funcionar sin problemas. Un saludo y discuplad las molestias.

Liberado Zend Framework 1.11 FINAL

Después de algunos meses de desarrollo, se ha liberado la final release de Zend Framework 1.11. Y parece ser que con interesantes novedades respecto a su predecesor:

  • Soporte a dispositivos móviles
  • SimpleCloud API
  • Diversas mejoras de seguridad
  • Mejorado el soporte a Dojo
  • Añadido soporte para Amazon’s SimpleDB
  • Añadido soporte para el API de Ebay Findings
  • Añadido soporte para el fork de MySQL, MariaDB
  • Añadidos nuevos formatos de configuración para el componente Zend_Config
  • Añadido servicios de URL Shortening y varios nuevos helpers para Zend_View.

Realmente tiene muy buena pinta y tengo ganas de probar estas nuevas mejoras lo antes posible!

Puede haber algo más bonito?

/**
 * A code written by Fabien Potencier and
 * shown during Barcelona PHP Conference 2010
 */
$method = 'getTitle';
$mapper = function ($article) use($method)
{
    return $article->$method();
};
$method = 'getAuthor'; $titles = array_map($mapper, $articles);

Para demostrar el uso de las closures, nueva característica introducida en PHP 5.3. Casi casi, parece poesía!

Y volvemos…

Después de un largo período de inactividad blogera me dispongo a retomar el blog que  en su día inicié y que acabé sustituyendo por una mini página de contacto personal. Todo ello motivado por la reciente Barcelona PHPConference 2010 y el profundo entusiasmo que me provocó algunas de las ponencias que se dieron.

zend certified engineer PHP 5.3 zend framework certified engineer

About me

Emprendedor, entusiasta y consultor web senior. Suscribo al manifiesto ágil, por que sobretodo creo en Personas e interacciones por encima de procesos y herramientas, Software que funciona por encima de documentación completa, Colaboración con el cliente por encima de negociación de contratos y Responder a los cambios por encima de seguir un plan.