Wednesday, August 11, 2010

Integrating Doctrine 2 and Zend Framework 1.10

In my search for a way of integrating Doctrine 2 with Zend Framework 1.10, I end up writing this guide based and inspired by this blog http://www.oelerich.org/?p=193

Here's the guide for integrating Doctrine 2 with Zend Framework 1.10

Installation
Fireup console and download all the sources of the ORM package by issuing this command:
$ git clone git://github.com/doctrine/doctrine2.git doctrine

next, update our Git checkout to use Doctrine\Common and Doctrine\DBAL package versions by these commands:
$ git submodule init
$ git submodule update

After all this boring checkout and updates, assuming we already have standard installation of Zend Framework, we then copy the following directories from the doctrine checkout to our zend framework installation.
doctrine/lib/Doctrine/ORM to ZendProject/library/Doctrine/ORM
doctrine/lib/vendor/doctrine-common/lib/Doctrine/Common to ZendProject/library/Doctrine/Common
doctrine/lib/vendor/doctrine-dbal/lib/Doctrine/DBAL toZendProject/library/Doctrine/DBAL

Copy the following doctrine files to our zend project:
doctrine/bin/doctrine to ZendProject/application/tools/doctrine
doctrine/bin/doctrine.php to ZendProject/application/tools/doctrine.php

Doctrine provide a tool for the command line interface, we can use these files to issue several doctrine commands such as generating a database schema based on our mapping files.

Bootstrapping Doctrine with Zend Framework bootstrap
In our application/Bootstrap.php file, create the following protected function.

protected function _initDoctrine()
{
    require '/Doctrine/Common/ClassLoader.php';
    
    $classLoader = new \Doctrine\Common\ClassLoader('Doctrine');
    $classLoader->register();
    
    if (APPLICATION_ENV == 'development') {
        $cache = new \Doctrine\Common\Cache\ArrayCache;
    } else {
        $cache = new \Doctrine\Common\Cache\ApcCache;
    }
    
    $entitiesPath = '../domain/Entities';
    $proxiesPath    = '../domain/proxies';
    
    $config = new \Doctrine\ORM\Configuration();
    $config->setMetadataCacheImpl($cache);
    $driverImpl = $config->newDefaultAnnotationDriver($entitiesPath);
    $config->setMetadataDriverImpl($driverImpl);
    $config->setQueryCacheImpl($cache);
    $config->setProxyDir($proxiesPath);
    $config->setProxyNamespace('domain\Proxies');
    
    if (APPLICATION_ENV == 'development') {
        $config->setAutoGenerateProxyClasses(true);
    } else {
        $config->setAutoGenerateProxyClasses(false);
    }
    
    $doctrineConfig = $this->getOption('doctrine');
    $connectionOptions = array(
        'driver'    => $doctrineConfig['conn']['driver'],
        'user'        => $doctrineConfig['conn']['user'],
        'pass'        => $doctrineConfig['conn']['pass'],
        'dbname'    => $doctrineConfig['conn']['dbname'],
        'host'        => $doctrineConfig['conn']['host']
    );
    
    $em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config);
    Zend_Registry::set('em', $em);
    
    return $em;
}

It's crucial to set the $entitiesPath with the location of our entities or models.

The $this->getOption('doctrine') gets our database connection options from our application/configs/application.ini or we can manually set the database config in the $connectionOptions array but the former is advisable as it is a good practice to place all configurations in one place.

After instantiating the entity manager with the connection options and doctrine config, we place it to registry so that we can have access to our entity manager anywhere.

The Entity
Create the following directories:
application/domain
application/domain/Entities
application/domain/Proxies

Let's have a User entity (application/domain/Entities/User.php) and place the following code:

/**
 * @Entity
 * @Table(name="user")
 */
class Entities_User
{
    /**
     * @Id @Column(type="integer")
     * @GeneratedValue(strategy="AUTO")
     */
    private $userId;
    
    /** @Column(type="string") */
    private $name;
    
    public function setName ($name)
    {
        $this->name = $name;
        return true;
    }
    
    public function getName()
    {
        return $this->name;
    }
}

This defines our entity User, there are several settings that we should specify in the annotations such as table name, fields, data types, etc. In our example, we will generate a database schema using this entity via Docblock Annotations.

Take note that Doctrine provides other ways for specifying object-relational mapping data and those are via XML or YAML. For more information visit http://www.doctrine-project.org/projects/orm/2.0/docs/reference/basic-mapping/en

Database Schema Generation
There are currently two way of generating the database schema, it's either via command line tool or using SchemaTool class.

The SchemaTool Class
Create a file named schema_tool.php in ZendProject/public/ directory with the following code below:

// Create database schema from entities.
$em = $application->getBootstrap()->getResource('doctrine');

$doctrineTool = new \Doctrine\ORM\Tools\SchemaTool($em);
$classes = array(
    $em->getClassMetadata('Entities_User')
);
$doctrineTool->createSchema($classes);

and include this file (schema_tool.php) at the bottom of ZendProject/public/index.php file after the

$application->bootstrap()
            ->run();
            
The Command Line Tool
In the ZendProject/application/tools/ directory, open up doctrine.php and set the require paths.
require_once '../../library/Doctrine/Common/ClassLoader.php';
require_once '../../library/Doctrine/Symfony/Components/Console/Helper/HelperSet.php';
require_once '../../library/Doctrine/Symfony/Components/Console/Application.php';

next create a file named cli-config.php in the same directory (ZendProject/application/tools/) with the following codes:

// Define path to your entities and proxies.
$entityPath = '../../domain/Entities';
$proxyPath    = '../../domain/Proxies';

// Register the namespace and include path of your entities to autoloader.
$classLoader = new \Doctrine\Common\ClassLoader('Entities', $entityPath);
$classLoader->register();

// Register the namespace and include path of your proxies to autoloader.
$classLoader = new \Doctrine\Common\ClassLoader('Proxies', $proxyPath);
$classLoader->register();

// Setup the configuration.
$config = new \Doctrine\ORM\Configuration();
$driverImpl = $config->newDefaultAnnotationDriver($entityPath);
$config->setMetadataDriverImpl($driverImpl);
$config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ArrayCache);
$config->setProxyDir($proxyPath);
$config->setProxyNamespace('domain\Proxies');

// Specify the connection options to your database.
$connectionOptions = array(
    'driver'    => 'pdo_mysql',
    'user'        => 'root',
    'pass'        => '',
    'dbname'    => 'test_db',
    'host'        => 'localhost'
);

// Get the entity manager.
$em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config);

$helperSet = new \Symfony\Components\Console\Helper\HelperSet(array(
    'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($em->getConnection()),
    'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em)
));

These codes are somewhat similar to the bootstrap method we created earlier, this file is utilized by the index.php file for CLI.

now in the command line, type:
$ php doctrine orm:schema-tool:create
and press enter, you will see a message "Database schema created successfully!". This will create all the database tables corresponding to the entities we created earlier.

Note: In a wamp environment, you need to reference your php binary first before the doctrine command
e.g. c:\wamp\bin\php\php5.3.0\php.exe doctrine orm:schema-tool:create

Now try the following code in your controller action:

public function indexAction()
{
   // action body
   $user = new Sms_Entities_User;
   $user->setName('Jeboy');
   $this->_em->persist($user);
   $this->_em->flush();
}

And that's it for integrating Doctrine 2 with Zend Framework 1.10. If you have better way of integration please let me know.

God bless :-)

13 comments:

  1. I'd love to hear your thoughts on dealing with Doctrine2 and Forms.

    I'm quite new to the whole ORM and DDD scene, and until recently I wasn't seeing the need for abandoning Zend_Db*. I do now, but I'm wondering if I'm seeing the Wrong Way of Doing Things.

    Should I care about a model service layer between my entities and everything else? When I use ZF forms I'm very much used to loading the rowset and then calling populate on my form. Naturally with Doctrine2 I can't do this without glue. I'm also a little confused as to how best to deal with select fields when they reference a relationship. It seems that the only way to deal with is something like $entity->getCategory()->getId().

    ReplyDelete
  2. I'm also quite new with Doctrine2, I haven't put the Doctrine ORM on testing with real world app. Currently in my experience, the easiest way is to construct a formData array from the entity object, of course this will be a tedious task if you are populating a large form with so many form fields.

    $formData = array(
    'name' => $user->getName()
    );
    $form->populate($formData);

    or by type casting:

    $user = (array) $this->_em->find('Entities_User', 1);
    $form->populate($user);

    but the result of type casting seems not friendly
    Array ( [�Entities_User�userId] => 1 [�Entities_User�name] => Jeboy )

    ReplyDelete
  3. by the way, we can also try to add toArray() method in our entity class and call that method in populating the form.

    e.g.
    class Entities_User
    {
    public function toArray()
    {
    return get_object_vars($this);
    }
    }

    $user = $this->_em->find('Entities_User', 1);
    $form->populate($user->toArray());

    ReplyDelete
  4. Thanks Jebb, I appreciate your thoughts :)

    ReplyDelete
  5. Hello Jeboy
    Thanks for the great tutorial, Since 4 days ago, I was looking for a good way to integrate the Doctrine 2 with Zend, but always I have a problem, I followed everything, but I have got 2 problems:
    1st when I run the code, I get only 500 Internal Server error, for your information, I use Zend Server with PHP 5.3 (Windows), while trying to comment and recognize which line causes the problem, in the end I understood it is from this line:
    $em = EntityManager::create($connectionOptions, $connectionOptions);
    Zend_Registry::set('em', $em);
    Don't worry if I did not write like your code \Doctrine\ORM\Entities\..., it is declared above
    (Path/to/my/project/application/tools/php doctrine

    C:/>Path/to/my/project/application/tools/
    First I tried in a working project, I had this result, when I am trying to set up the D2 with ZF1 following your tutorial, I get the same result (Blank line only).
    So I guess there is a misconfiguration in my Zend Server, Do you have any idea sir?
    Will be so grateful.

    Kind regards,

    Naoufal

    ReplyDelete
  6. I am watching this project https://github.com/eddiejaoude/Zend-Framework--Doctrine-ORM--PHPUnit--Ant--Jenkins-CI--TDD- they are doing some interesting stuff, similar to yours. They have implemented a basic authentication system using Zend_Auth & Doctrine2.

    ReplyDelete
  7. @a thanks for the useful info., by the way you can also check this out https://github.com/guilhermeblanco/ZendFramework1-Doctrine2 it would be also helpful.

    ReplyDelete
  8. Can I have your complete source for this one ? I tried but I failed on


    $ php doctrine orm:schema-tool:create

    it says:


    H:\project\TESTDO~1\application\tools>php doctrine orm:schema-tool:create

    Warning: require(Doctrine\ORM\Configuration.php): failed to open stream: No such file or directory in H:\project\testdoc
    trine\library\Doctrine\Common\ClassLoader.php on line 148

    Fatal error: require(): Failed opening required 'Doctrine\ORM\Configuration.php' (include_path='.;H:\xampplite\php\PEAR'
    ) in H:\project\testdoctrine\library\Doctrine\Common\ClassLoader.php on line 148


    Where I should place include path ?
    I am using doctrine 2.0 with zf 1.11x

    ReplyDelete
  9. Hello, maybe I missed something but what for is cli-doctrine.php ?
    We are using doctrine.php to load models... ?

    ReplyDelete
  10. Sorry, but I also have a problem with
    require(Doctrine/ORM/Configuration.php)...file to open stream - I made the same folders like in your tutorial. Could you please let me know where is the problem ?

    ReplyDelete
  11. 'pass' => $doctrineConfig['conn']['pass'],

    should be

    'password' => $doctrineConfig['conn']['pass'],

    ReplyDelete
  12. Where has this Sms_ come from ?

    ReplyDelete
  13. I'm sorry guys I wasn't able to answer your questions, really2x busy till now.

    ReplyDelete