I’ve begun to investigate the Doctrine ORM library and how to integrate it into Zend Framework applications. I figure this post and any subsequent ones on the topic can grow into some sort of discovery series.

So, my first post is going to be a simple one – bootstrapping with Doctrine 1.2.

I figured the most robust and consistent way to get Doctrine into my applications was to create an application resource plugin so without further delay, here it is.

// library/Tolerable/Application/Resource/Doctrine.php
 
class Tolerable_Application_Resource_Doctrine extends Zend_Application_Resource_ResourceAbstract
{
    /**
     * @var Doctrine_Manager
     */
    protected $_manager;
 
    /**
     * @var Doctrine_Cache_Interface
     */
    protected $_cacheDriver;
 
    /**
     * @var string|array
     */
    protected $_cache;
 
    /**
     * @var array
     */
    protected $_connections = array();
 
    /**
     * Set cache options
     * 
     * @param string|array $cache
     * @return Tolerable_Application_Resource_Doctrine
     */
    public function setCache($cache)
    {
        $this->_cache = $cache;
        return $this;
    }
 
    /**
     * @return string|array
     */
    public function getCache()
    {
        return $this->_cache;
    }
 
    /**
     * Set connection configuration
     * 
     * @param array $connections
     * @return Tolerable_Application_Resource_Doctrine
     */
    public function setConnections(array $connections)
    {
        $this->_connections = $connections;
        return $this;
    }
 
    /**
     * Initialise and return Doctrine_Manager instance
     * 
     * @return Doctrine_Manager
     */
    public function getManager()
    {
        if (null === $this->_manager) {
            $manager = Doctrine_Manager::getInstance();
            $manager->setAttribute(Doctrine_Core::ATTR_MODEL_LOADING,
                                   Doctrine_Core::MODEL_LOADING_CONSERVATIVE);
            $manager->setAttribute(Doctrine_Core::ATTR_VALIDATE,
                                   Doctrine_Core::VALIDATE_ALL);
            $manager->setAttribute(Doctrine_Core::ATTR_AUTO_ACCESSOR_OVERRIDE, true);
            $manager->setAttribute(Doctrine_Core::ATTR_AUTOLOAD_TABLE_CLASSES, true);
            $manager->setAttribute(Doctrine_Core::ATTR_USE_DQL_CALLBACKS, true);
            $manager->setAttribute(Doctrine_Core::ATTR_AUTO_FREE_QUERY_OBJECTS, true);
 
            if ($cacheDriver = $this->_getCacheDriver()) {
                $manager->setAttribute(Doctrine_Core::ATTR_QUERY_CACHE, $cacheDriver);
            }
 
            $this->_manager = $manager;
        }
        return $this->_manager;
    }
 
    /**
     * Initialise and return query cache driver if configured
     * 
     * @return Doctrine_Cache_Interface|null
     */
    protected function _getCacheDriver()
    {
        if (null === $this->_cacheDriver) {
            $options = $this->getCache();
            if (empty($options)) {
                return null;
            }
 
            if (is_array($options)) {
                if (!array_key_exists('driver', $options)) {
                    throw new Zend_Application_Resource_Exception('Missing Doctrine cache driver');
                }
                $driver = $options['driver'];
                unset($options['driver']);
            } else {
                $driver = (string) $options;
                $options = array();
            }
 
            if (!class_exists($driver)) {
                Zend_Loader::loadClass($driver);
            }
 
            $this->_cacheDriver = new $driver($options);
        }
        return $this->_cacheDriver;
    }
 
    /**
     * Initialise any configured connections
     * 
     * @return void
     */
    protected function _initConnections()
    {
        foreach ($this->_connections as $name => $options) {
            if (!is_array($options)) {
                $options = array('dsn' => $options);
            }
 
            if (array_key_exists('charset', $options)) {
                $charset = $options['charset'];
                unset($options['charset']);
            } else {
                $charset = 'utf8';
            }
 
            // Attempt to find "dsn" or "connection_string" keys
            // falling back to an array of connection options
            if (array_key_exists('dsn', $options)) {
                $adapter = $options['dsn'];
            } else if (array_key_exists('connection_string', $options)) {
                $adapter = $options['connection_string'];
            } else {
                $adapter = $options;
            }
 
            $connection = $this->getManager()->openConnection($adapter, $name);
            $connection->setAttribute(Doctrine_Core::ATTR_USE_NATIVE_ENUM, true);
            $connection->setCharset($charset);
        }
    }
 
    /**
     * Initialise and return Doctrine manager
     * 
     * @return Doctrine_Manager
     */
    public function init()
    {
        $this->_initConnections();
        return $this->getManager();
    }
}

This class provides a reusable component to use in any ZF 1.8+ application. To enable the plugin, add the following to your application.ini file.

; Add the Doctrine and Tolerable namespaces to the autoloader
autoloaderNamespaces[] = "Doctrine_"
autoloaderNamespaces[] = "Tolerable_"
 
; Add our custom application resource plugin path to the plugin loader
pluginPaths.Tolerable_Application_Resource = "Tolerable/Application/Resource"

Configure the Doctrine query cache (if desired)

; Simple examples, no driver options
resources.doctrine.cache = "Doctrine_Cache_Apc"
 
; or with options
resources.doctrine.cache.driver = "Doctrine_Cache_Apc"
resources.doctrine.cache.option = ...
resources.doctrine.cache.option = ...
resources.doctrine.cache.option = ...

See here for drivers and options.

Configure the connections

; Simple connection strings
resources.doctrine.connections[] = "mysql://user1:pass@localhost/db1"
resources.doctrine.connections[] = "mysql://user2:pass@localhost/db2"
 
; or specify connection name and options
resources.doctrine.connections.default.dsn     = "mysql://user1:pass@localhost/db1"
resources.doctrine.connections.default.charset = "utf8"
 
; or specify Doctrine connection parameters
resources.doctrine.connections.default.scheme = "mysql"
resources.doctrine.connections.default.user   = "user1"
resources.doctrine.connections.default.pass   = "pass"
resources.doctrine.connections.default.host   = "localhost"
resources.doctrine.connections.default.path   = "db1"

This is assuming that your custom (Tolerable in these examples) and Doctrine libraries are under your application’s “library” path. If not, simply add them to the application’s include path configuration, for example

includePaths.library = APPLICATION_PATH "/../library"
includePaths.doctrine = "/path/to/doctrine/lib"
includePaths.custom = "/path/to/custom/library"

A lot of the code in the plugin has been taken from Matthew Weier O’Phinney’s blog post on Autoloading Doctrine and Doctrine entities from Zend Framework.

2009-11-19 Update

Some extra inspiration came from Juozas Kaziukenas’ post on Zend Framework and Doctrine. Part 2 as well as the twitter-verse and now the plugin handles multiple connections and caching.