Doctrine ORM Module for Zend Framework

This module works with Zend Framework 2 and 3.

Zend Developer Tools in DoctrineORMModule

If you ever tried Zend Developer Tools you will surely understand the importance of being able to track performance pitfalls or excessive amount of queries in your applications when developing.

Setup

To setup Zend Developer Tools, run

composer require zendframework/zend-developer-tools

Then enable ZendDeveloperTools in your modules and enable profiling and the toolbar (see docs of Zend Developer Tools for that).

Once ZendDeveloperTools is enabled, having doctrine.entity_manager.orm_default as your default EntityManager, you will notice that the queries performed by the ORM get logged and displayed in the toolbar.

Customization

If you want to customize this behavior (or track multiple EntityManager instances) you can do it in different ways. Please note that if you have set an SQLLogger in your configuration, this functionality won’t override it, so you can use these features in total safety.

Multiple EntityManager/Connection instances and logging

WARNING! These are advanced features! Even if the code is fully tested, this is usually not required for most users!

To setup logging for an additional DBAL Connection or EntityManager, put something like following in your module:

<?php

namespace MyNamespace;

class Module
{
    public function getConfig()
    {
        return [
            'doctrine' => [
                'sql_logger_collector' => [
                    'other_orm' => [
                        // name of the sql logger collector (used by ZendDeveloperTools)
                        'name' => 'other_orm',

                        // name of the configuration service at which to attach the logger
                        'configuration' => 'doctrine.configuration.other_orm',

                        // uncomment following if you want to use a particular SQL logger instead of relying on
                        // the attached one
                        //'sql_logger' => 'service_name_of_my_dbal_sql_logger',
                    ],
                ],
            ],

            'zenddevelopertools' => [

                // registering the profiler with ZendDeveloperTools
                'profiler' => [
                    'collectors' => [
                        // reference to the service we have defined
                        'other_orm' => 'doctrine.sql_logger_collector.other_orm',
                    ],
                ],

                // registering a new toolbar item with ZendDeveloperTools (name must be the same of the collector name)
                'toolbar' => [
                    'entries' => [
                        // this is actually a name of a view script to use - you can use your custom one
                        'other_orm' => 'zend-developer-tools/toolbar/doctrine-orm',
                    ],
                ],
            ],
        ];
    }

    public function getServiceConfiguration()
    {
        return [
            'factories' => [
                // defining a service (any name is valid as long as you use it consistently across this example)
                'doctrine.sql_logger_collector.other_orm' => new \DoctrineORMModule\Service\SQLLoggerCollectorFactory('other_orm'),
            ],
        ];
    }

    public function onBootstrap(\Zend\EventManager\EventInterface $e)
    {
        $config = $e->getTarget()->getServiceManager()->get('Config');

        if (isset($config['zenddevelopertools']['profiler']['enabled'])
            && $config['zenddevelopertools']['profiler']['enabled']
        ) {
            // when ZendDeveloperTools is enabled, initialize the sql collector
            $app->getServiceManager()->get('doctrine.sql_logger_collector.other_orm');
        }
    }
}

This example will simply generate a new icon in the toolbar, with the log results of your other_orm connection:

Configuration

Register a Custom DQL Function

return [
    'doctrine' => [
        'configuration' => [
            'orm_default' => [
                'numeric_functions' => [
                    'ROUND' => 'Db\DoctrineExtensions\Query\Mysql\Round',
                ],
            ],
        ],
    ],
];

Register a Type mapping

return [
    'doctrine' => [
        'connection' => [
            'orm_default' => [
                'doctrine_type_mappings' => [
                    'enum' => 'string',
                ],
            ],
        ],
    ],
];

How to add new type

return [
    'doctrine' => [
        'configuration' => [
            'orm_default' => [
                'types' => [
                    'newtype' => 'Db\DBAL\Types\NewType',
                ],
            ],
        ],
    ],
];
return [
    'doctrine' => [
        'connection' => [
            'orm_default' => [
                'doctrine_type_mappings' => [
                    'mytype' => 'mytype',
                ],
            ],
        ],
    ],
];

Doctrine Type Comment

Option to set the doctrine type comment (DC2Type:myType) for custom types

return [
    'doctrine' => [
        'connection' => [
            'orm_default' => [
                'doctrineCommentedTypes' => [
                    'mytype',
                ],
            ],
        ],
    ],
];

Built-in Resolver

How to Define Relationships with Abstract Classes and Interfaces (ResolveTargetEntityListener)

return [
    'doctrine' => [
        'entity_resolver' => [
            'orm_default' => [
                'resolvers' => [
                    'Acme\\InvoiceModule\\Model\\InvoiceSubjectInterface',
                    'Acme\\CustomerModule\\Entity\\Customer',
                ],
            ],
        ],
    ],
];

Set a Custom Default Repository

return [
    'doctrine' => [
        'configuration' => [
            'orm_default' => [
                'default_repository_class_name' => 'MyCustomRepository',
            ],
        ],
    ],
];

How to Use Two Connections

See also this blog article.

return [
    'doctrine' => [
        'connection' => [
            'orm_crawler' => [
                'driverClass'   => 'Doctrine\DBAL\Driver\PDOMySql\Driver',
                'eventmanager'  => 'orm_crawler',
                'configuration' => 'orm_crawler',
                'params'        => [
                    'host'     => 'localhost',
                    'port'     => '3306',
                    'user'     => 'root',
                    'password' => 'root',
                    'dbname'   => 'crawler',
                    'driverOptions' => [
                        1002 => 'SET NAMES utf8',
                    ],
                ],
            ],
        ],

        'configuration' => [
            'orm_crawler' => [
                'metadata_cache'    => 'array',
                'query_cache'       => 'array',
                'result_cache'      => 'array',
                'hydration_cache'   => 'array',
                'driver'            => 'orm_crawler_chain',
                'generate_proxies'  => true,
                'proxy_dir'         => 'data/DoctrineORMModule/Proxy',
                'proxy_namespace'   => 'DoctrineORMModule\Proxy',
                'filters'           => [],
            ],
        ],

        'driver' => [
            'orm_crawler_annotation' => [
                'class' => 'Doctrine\ORM\Mapping\Driver\AnnotationDriver',
                'cache' => 'array',
                'paths' => [
                    __DIR__ . '/../src/Crawler/Entity',
                ],
            ],
            'orm_crawler_chain' => [
                'class'   => 'Doctrine\ORM\Mapping\Driver\DriverChain',
                'drivers' => [
                    'Crawler\Entity' =>  'orm_crawler_annotation',
                ],
            ],
        ],

        'entitymanager' => [
            'orm_crawler' => [
                'connection'    => 'orm_crawler',
                'configuration' => 'orm_crawler',
            ],
        ],

        'eventmanager' => [
            'orm_crawler' => [],
        ],

        'sql_logger_collector' => [
            'orm_crawler' => [],
        ],

        'entity_resolver' => [
            'orm_crawler' => [],
        ],
    ],
];

The DoctrineModule\ServiceFactory\AbstractDoctrineServiceFactory will create the following objects as needed: * ‘doctrine.connection.orm_crawler’ * ‘doctrine.configuration.orm_crawler’ * ‘doctrine.entitymanager.orm_crawler’ * ‘doctrine.driver.orm_crawler’ * ‘doctrine.eventmanager.orm_crawler’ * ‘doctrine.entity_resolver.orm_crawler’ * ‘doctrine.sql_logger_collector.orm_crawler’

You can retrieve them from the service manager via their keys.

How to Use Naming Strategy

Official documentation

Zend Configuration

return [
    'service_manager' => [
        'invokables' => [
            'Doctrine\ORM\Mapping\UnderscoreNamingStrategy' => 'Doctrine\ORM\Mapping\UnderscoreNamingStrategy',
        ],
    ],
    'doctrine' => [
        'configuration' => [
            'orm_default' => [
                'naming_strategy' => 'Doctrine\ORM\Mapping\UnderscoreNamingStrategy',
            ],
        ],
    ],
];

How to Use Quote Strategy

Official documentation

Zend Configuration

return [
    'service_manager' => [
        'invokables' => [
            'Doctrine\ORM\Mapping\AnsiQuoteStrategy' => 'Doctrine\ORM\Mapping\AnsiQuoteStrategy',
        ],
    ],
    'doctrine' => [
        'configuration' => [
            'orm_default' => [
                'quote_strategy' => 'Doctrine\ORM\Mapping\AnsiQuoteStrategy',
            ],
        ],
    ],
];

Caching

Caching is very important in Doctrine.

In this example for Metadata, Queries, and Results we set an array cache for the result_cache. Please note the array cache is for development only and shown here along side other cache types.

If you want to set a cache for query, result and metadata, you can specify this inside your config/autoload/local.php

return [
    'doctrine' => [
        'configuration' => [
            'orm_default' => [
                'query_cache'       => 'filesystem',
                'result_cache'      => 'array',
                'metadata_cache'    => 'apc',
                'hydration_cache'   => 'memcached',
            ],
        ],
    ],
];

The previous configuration takes into consideration different cache adapters. You can specify any other adapter that implements the Doctrine\Common\Cache\Cache interface. Find more here.

Example with Redis

return [
    'doctrine' => [
        'configuration' => [
            'orm_default' => [
                'query_cache'       => 'redis',
                'result_cache'      => 'redis',
                'metadata_cache'    => 'redis',
                'hydration_cache'   => 'redis',
            ],
        ],
    ],
];

In this case you have to specify a custom factory in your service_manager configuration to create a Redis object:

// module.config.php
return [
    'service_manager' => [
        'factories' => [
            __NAMESPACE__ . '\Cache\Redis' => __NAMESPACE__ . '\Cache\RedisFactory',
        ],
    ],
    'doctrine' => [
        'cache' => [
            'redis' => [
                'namespace' => __NAMESPACE__ . '_Doctrine',
                'instance'  => __NAMESPACE__ . '\Cache\Redis',
            ],
        ],
    ],
];
// RedisFactory.php
namespace YourModule\Cache;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class RedisFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $redis = new Redis();
        $redis->connect('127.0.0.1', 6379);

        return $redis;
    }
}

Read more about Caching.

How to enable and configure Second Level Cache

return [
    'doctrine' => [
        'configuration' => [
            'orm_default' => [
                'result_cache' => 'redis', // Second level cache reuse the cache defined in result cache
                'second_level_cache' => [
                    'enabled'               => true,
                    'default_lifetime'      => 200,
                    'default_lock_lifetime' => 500,
                    'file_lock_region_directory' => __DIR__ . '/../my_dir',
                    'regions' => [
                        'My\FirstRegion\Name' => [
                            'lifetime'      => 800,
                            'lock_lifetime' => 1000,
                        ],
                        'My\SecondRegion\Name' => [
                            'lifetime'      => 10,
                            'lock_lifetime' => 20,
                        ],
                    ],
                ],
            ],
        ],
    ],
];

You also need to add the Cache annotation to your model (read more). Read more about Second Level Cache.

Doctrine Migrations

Support for the migrations library is included. Only one migration configuration is possible.

Configure

return [
    'doctrine' => [
        'migrations_configuration' => [
            'orm_default' => [
                'directory' => 'path/to/migrations/dir',
                'name' => 'Migrations Name',
                'namespace' => 'Migrations  Namespace',
                'table' => 'migrations_table',
                'column' => 'version',
                'custom_template' => null,
            ],
        ],
    ],
];

Multiple Migration Configurations

At this time if you want to have migrations for multiple entity manager database configurations you must use the .phar archive and external configuration files.

Miscellaneous

The items listed below are optional and intended to enhance integration between Zend Framework and Doctrine 2.

ObjectExists Validator and NoObjectExists Validator

ObjectExists and NoObjectExists are validators similar to Zend Validators. You can pass a variety of options to determine validity. The most basic use case requires an entity manager, an entity, and a field. You also have the option of specifying a query_builder Closure to use if you want to fine tune the results.

<?php
$validator = new \DoctrineModule\Validator\NoObjectExists([
    // object repository to lookup
    'object_repository' => $serviceLocator->get('doctrine.entitymanager.orm_default')
        ->getRepository('Db\Entity\User'),

    // fields to match
    'fields' => ['username'],
]);

// following works also with simple values if the number of fields to be matched is 1
echo $validator->isValid(['username' => 'test']) ? 'Valid' : 'Invalid. A duplicate was found.';

Authentication Adapter

The authentication adapter is intended to provide an adapter for Zend\Authentication. It works much like the DbTable adapter in the core framework. You must provide the entity manager instance, entity name, identity field, and credential field. You can optionally provide a callable method to perform hashing on the password prior to checking for validation.

<?php

use DoctrineModule\Authentication\Adapter\DoctrineObject as DoctrineObjectAdapter;

$adapter = DoctrineObjectAdapter(
    $entityManager,
    'Application\Test\Entity',
    'username', // optional, default shown
    'password',  // optional, default shown,
    function($identity, $credential) { // optional callable
        return \Application\Service\User::hashCredential(
            $credential,
            $identity->getSalt(),
            $identity->getAlgorithm()
        );
    }
);

$adapter->setIdentityValue('admin');
$adapter->setCredentialValue('password');
$result = $adapter->authenticate();

echo $result->isValid() ? 'Authenticated' : 'Could not authenticate';

Custom DBAL Types

To register custom Doctrine DBAL types add them to the doctrine.configuration.orm_default.types key in you configuration file:

<?php
return [
    'doctrine' => [
        'configuration' => [
            'orm_default' => [
                'types' => [
                    // You can override a default type
                    'date' => 'My\DBAL\Types\DateType',

                    // And set new ones
                    'tinyint' => 'My\DBAL\Types\TinyIntType',
                ],
            ],
        ],
    ],
];

With this configuration you may use them in your ORM entities to define field datatypes:

<?php

class User
{
    /**
     * @ORM\Column(type="date")
     */
    protected $birthdate;

    /**
     * @ORM\Column(type="tinyint")
     */
    protected $houses;
}

To have Schema-Tool convert the underlying database type of your new “tinyint” directly into an instance of TinyIntType you have to additionally register this mapping with your database platform.

<?php
return [
    'doctrine' => [
        'connection' => [
            'orm_default' => [
                'doctrine_type_mappings' => [
                    'tinyint' => 'tinyint',
                ],
            ],
        ],
    ],
];

Now using Schema-Tool, whenever it finds a column of type “tinyint” it will convert it into a “tinyint” Doctrine Type instance for Schema representation. Keep in mind that you can easily produce clashes this way because each database type can only map to exactly one Doctrine mapping type.