Doctrine 2 on Zend framework

This article describes how to connect Doctrine2 with Zend Framework.

Main implementation stuff:

Update 2010-12-18

First of all required libraries and file structure:

Note:

The file structure should look like this now:

...
/myapp
    /application
        /configs
            /Mapping
            /Migration
            application.ini
        /Entity
        /modules
        /Proxy
        Bootstrap.php
    /library
        /Doctrine
            /Common
            /DBAL
                /Migrations
                ...
            /ORM
            /Symfony
        /Zend /*if not in include path*/
    /public
...

Next, will use yaml mapping for our entities in this example, create a User entity in Yaml mapping format.

---
# FILE /myapp/application/configs/Mapping/Entity.User.dcm.yml
Entity\User:
  type: entity
  table: users
  id:
    id:
      type: integer
      generator:
        strategy: AUTO
  fields:
    password:
      type: string
      length: 32
    username:
      type: string
      length: 128
  oneToOne:
    role:
      targetEntity: Entity\Role
      inversedBy: users
      joinColumn:
        name: role_id
        referencedColumnName: id
  indexes:
    search_idx:
      columns: username

The other one is Role Entity. Location /myapp/application/configs/Mapping/

---
# FILE /myapp/application/configs/Mapping/Entity.Role.dcm.yml
Entity\Role:
  type: entity
  table: roles
  id:
    id:
      type: integer
      generator:
        strategy: AUTO
  fields:
    name:
      type: string
      length: 50
      unique: true
  oneToMany:
    users:
      targetEntity: Entity\User
      mappedBy: role

Before we start using cli to create our tables and Entities, we need to modify the /myapp/configs/application.ini

[production]
; here goes some standard configuration
phpSettings.display_startup_errors = 0
phpSettings.display_errors = 0

bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
bootstrap.class = "Bootstrap"

; we will use the module for the good practise
resources.frontController.moduleDirectory = APPLICATION_PATH "/modules"
resources.frontController.moduleDefault = "default"

; layout if needed
resources.layout.layout = "layout"
resources.view[] =
; -------------------------------------
; Here fallows Doctrine 2 Configuration
; -------------------------------------

; Database configuration
dbal.driver = "pdo_mysql"
dbal.host = "127.0.0.1"
dbal.user = "root"
dbal.password = "secret"
dbal.dbname = "doctrine2"

; location for Entity proxies and namespace
orm.proxy.path = APPLICATION_PATH "/Proxy"
orm.proxy.namespace = "Proxy"

; ----------------------------------------------------
; Multi driver configuration for our database metadata
; ----------------------------------------------------

; first is the yaml driver to read the mapping from User.yml
orm.driver.mainYamlDriver.type = "Yaml"
orm.driver.mainYamlDriver.path[] = APPLICATION_PATH "/configs/Mapping"
orm.driver.mainYamlDriver.namespace = "Entity"

; second is the annotation driver for example for mapping extension Entities
; notice: that the namespace should differ and the location specified to look
; for Entity metadata should exist
;orm.driver.extensionDriver.type = "Annotation"
;orm.driver.extensionDriver.path[] = APPLICATION_PATH "/../library/DoctrineExtensions/Versionable/Entity"
;orm.driver.extensionDriver.namespace = "DoctrineExtensions"

; later use APC for instance
orm.cache.metadata = "Array"
orm.cache.query = "Array"

[staging : production]

[cli : production]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1

[testing : production]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1

[development : production]
phpSettings.display_startup_errors = 1
phpSettings.display_errors = 1
resources.frontController.params.displayExceptions = 1

Modify the /myapp/application/Boostrap.php and add doctrine boot

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
    protected function _initDoctrine()
    {
        // load the doctrine class loader for Doctrine library
        require 'Doctrine/Common/ClassLoader.php';
        $classLoader = new Doctrine\Common\ClassLoader('Doctrine');
        $classLoader->register();
        // load the doctrine class loader for Symfony library components
        $classLoader = new \Doctrine\Common\ClassLoader('Symfony', 'Doctrine');
        $classLoader->register();
        // load the doctrine class loader for Entity autoloading
        $classLoader = new \Doctrine\Common\ClassLoader(
            'Entity',
            APPLICATION_PATH
        );
        $classLoader->register();
        // read ini configuration
        $settings = $this->getOption('orm');

        // load proxy configuration settings
        $config = new Doctrine\ORM\Configuration;
        $config->setProxyDir($settings['proxy']['path']);
        $config->setProxyNamespace($settings['proxy']['namespace']);
        $config->setAutoGenerateProxyClasses((APPLICATION_ENV == "development"));
        // load metadata drivers
        $drivers = $settings['driver'];
        $chainDriverImpl = new Doctrine\ORM\Mapping\Driver\DriverChain();
        foreach ($drivers as $driver) {
            if ($driver['type'] == 'Annotation') {
                $driverImpl = $config->newDefaultAnnotationDriver($driver['path']);
            } else {
                $driverClassName = 'Doctrine\ORM\Mapping\Driver\\' . $driver['type'] . 'Driver';
                $driverImpl = new $driverClassName($driver['path']);
            }
            $chainDriverImpl->addDriver($driverImpl, $driver['namespace']);
        }
        $config->setMetadataDriverImpl($chainDriverImpl);
        // Set up caches
        $cacheMetadataClassName = 'Doctrine\Common\Cache\\' . $settings['cache']['metadata'] . 'Cache';
        $cache = new $cacheMetadataClassName();
        $config->setMetadataCacheImpl($cache);
        if ($settings['cache']['metadata'] != $settings['cache']['query']) {
            $cacheQueryClassName = 'Doctrine\Common\Cache\\' . $settings['cache']['query'] . 'Cache';
            $cache = new $cacheQueryClassName();
        }
        $config->setQueryCacheImpl($cache);

        // event manager if needed
        $evm = new Doctrine\Common\EventManager();
        // boot entity manager
        $em = Doctrine\ORM\EntityManager::create(
            $this->getOption('dbal'),
            $config,
            $evm
        );
        // store entity manager in registry
        Zend_Registry::set('em', $em);
        return $em;
    }
}

The metadata and database is the best to create through Doctrine CLI. Create the /myapp/scripts/ directory and add the doctrine.php file in it, which will boostrap the CLI

<?php
/**
 * FILE: /myapp/scripts/doctrine.php
 * Doctrine 2 CLI script
 */

define('APPLICATION_ENV', 'cli');
define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));

set_include_path(implode(PATH_SEPARATOR, array(
    realpath(APPLICATION_PATH . '/../library'),
    get_include_path(),
)));

require_once 'Zend/Application.php';

// Create application, bootstrap, and run
$application = new Zend_Application(
    APPLICATION_ENV,
    APPLICATION_PATH . '/configs/application.ini'
);

$application->getBootstrap()->bootstrap('doctrine');
$em = Zend_Registry::get('em');

// comment if migrations are not used
$classLoader = new Doctrine\Common\ClassLoader('Doctrine\DBAL\Migrations', 'Doctrine/DBAL');
$classLoader->register();

$helpers = array(
    'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($em->getConnection()),
    'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em),
    'dialog' => new \Symfony\Component\Console\Helper\DialogHelper(),
);

$cli = new \Symfony\Component\Console\Application(
    'Doctrine Command Line Interface',
    Doctrine\ORM\Version::VERSION
);
$cli->setCatchExceptions(true);
$helperSet = $cli->getHelperSet();
foreach ($helpers as $name => $helper) {
    $helperSet->set($helper, $name);
}

$cli->addCommands(array(
    // DBAL Commands
    new \Doctrine\DBAL\Tools\Console\Command\RunSqlCommand(),
    new \Doctrine\DBAL\Tools\Console\Command\ImportCommand(),

    // ORM Commands
    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(),

    // Migrations Commands, remove if not needed
    new \Doctrine\DBAL\Migrations\Tools\Console\Command\DiffCommand(),
    new \Doctrine\DBAL\Migrations\Tools\Console\Command\ExecuteCommand(),
    new \Doctrine\DBAL\Migrations\Tools\Console\Command\GenerateCommand(),
    new \Doctrine\DBAL\Migrations\Tools\Console\Command\MigrateCommand(),
    new \Doctrine\DBAL\Migrations\Tools\Console\Command\StatusCommand(),
    new \Doctrine\DBAL\Migrations\Tools\Console\Command\VersionCommand(),
));
$cli->run();

If you use migrations create ../myapp/scripts/migrations.yml for migration configuration:

---
name: Doctrine Migrations
migrations_namespace: DoctrineMigration
table_name: ext_migration_versions
migrations_directory: ../application/configs/Migration

Create the shell or batch script(windows) to run it through PHP cli

/myapp/scripts/doctrine.bat dont forget to modify the php.exe path and the script location if it difers

@echo off
echo Running Doctrine CLI.
"C:\php\php.exe" -f doctrine.php %1 %2 %3 %4 %5 %6 %7 %8 %9

For linux users

/myapp/scripts/doctrine.sh dont forget to modify the php bin path if it difers, and run chmod +x

#!/usr/bin/env php
chdir(dirname(__FILE__));
include('doctrine.php');

Now lets go to the Shell or Command Prompt, go to /myapp/scripts/ and run these commands:

Notice: that the last argument(1, 2 commands) is the location of your application

Notice: before running the third command, database must be created, empty and entities must be created also[first command]

After running first command the Entities should be created in the /application/Entity directory, unless you specified another. After running the second would create proxies for the lazy loading. Then if you used migrations, 3 command would create migration from the entities found and 4 command would migrate to these changes.

By this point, we should have everything in place. Lets play around a bit. First, if you have not created index controller, create it and modify a bit to test our new stuff, file: /myapp/application/modules/default/controllers/IndexController.php

class IndexController extends Zend_Controller_Action
{
    protected $_em;

    public function init()
    {
        $this->_em = Zend_Registry::get('em');
    }

    public function indexAction()
    {
        $user = $this->_em->getRepository('Entity\User')->find(1);
        if (!$user) {
            // create groups and user only once
            $this->populate();
            $user = $this->_em->getRepository('Entity\User')->find(1);
        }
        $this->view->user = $user;
    }

    public function populate()
    {
        // create Roles
        $guestRole = new Entity\Role;
        $guestRole->setName('guest');
        $this->_em->persist($guestRole);

        $adminRole = new Entity\Role;
        $adminRole->setName('admin');
        $this->_em->persist($adminRole);

        $memberRole = new Entity\Role;
        $memberRole->setName('member');
        $this->_em->persist($memberRole);

        // create users
        $user = new Entity\User;
        $user->setUsername('Master');
        $user->setPassword('secret');
        $user->setRole($adminRole);
        $this->_em->persist($user);

        $this->_em->flush();
    }
}

Now if you run index action few groups will be created and an user, which is passed to the view: /application/modules/default/view/index/index.phtml

hello <php? echo $this->user->getUsername(); ?>

Also probably layout is required, create:

<!-- FILE: ../myapp/modules/default/views/scripts/layout.phtml -->
<!DOCTYPE html>
<html>
<head>
</head>
<body>
    <div id="content">
        <?php echo $this->layout()->content; ?>
    </div>
</body>
</html>

If something went wrong or you cannot understand some parts of code, you can download the application which was made in this example. Notice: that it will require Zend library in the /myapp/library folder if you do not have it in the include path. Also you will need to adjust the database credentials.

Thats it. Now you can easily add another features and enjoy the Doctrine 2 on Zend framework