Michaël Gallego

This is my blog. What can you expect here? Well... Zend Framework 2, Amazon AWS...

Twitter

Google+

LinkedIn

Github

Last.fm

Flexible multi db-adapter architecture for generic modules in ZF 2

Pour la version française de cet article, c’est par ici !

For the Russian version, follow this link (thanks to Oleg Lobach) !

I’m currently developing a Forum module for Zend Framework 2. The main goal when designing a generic module is to be able to quickly switch from different database adapters (Zend\Db, Doctrine ORM, Doctrine Mongo…) without rewriting everything.

Currently, the module is only based on Doctrine ORM but I would like it to support Zend\Db as well as Doctrine Mongo, so that ZfrForum would be the “default” module with Zend\Db, ZfrForumDoctrineORM the module with DoctrineORM support, and ZfrForumDoctrineODM the module with Doctrine Mongo support.

In this article, I’ll show you how I solved this problem, and how abstract factories (one of the least known ZF 2 features, I suppose !) can drastically reduce the lines you need to write.

Principle

The base idea is rather simple : we are going to create mapper interfaces, and concrete implementations for every db adapter we want to support (Zend\Db, Doctrine ORM, Doctrine ODM…).

Services will consume those mappers by using the interface (never ever a concrete implementation !), so that the service manager will inject either a Zend\Db mapper, or a Doctrine ORM entity repository.

Interfaces

First, we are going to create an interface that represents a single mapper. For those who don’t know, a mapper is basically an object that will interact with the database, and return objects. For instance, here is a simple mapper to deal with categories :

namespace ZfrForum\Mapper;

use ZfrForum\Entity\Category;

interface CategoryMapperInterface
{
    /**
     * @param  Category $category
     * @return mixed
     */
    public function create(Category $category);

    /**
     * @param  Category $category
     * @return mixed
     */
    public function update(Category $category);
}

Concrete implementations

Now that we have an interface, let’s write the concrete classes.

By default, this is a mapper that uses Zend\Db internally (I’ve never used Zend\Db myself, so the code may be a little inaccurate, sorry about that !).

namespace ZfrForum\Repository;

use Zend\Db\TableGateway\TableGatewayInterface;
use ZfrForum\Entity\Category;
use ZfrForum\Mapper\CategoryMapperInterface;

class CategoryMapper implements CategoryMapperInterface
{
    /**
     * @var TableGatewayInterface
     */
    protected $tableGateway;

    public function __construct(TableGatewayInterface $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    /**
     * @param  Category $category
     * @return mixed
     */
    public function create(Category $category)
    {
        // TODO: Implement create() method.
    }

    /**
     * @param  Category $category
     * @return mixed
     */
    public function update(Category $category)
    {
        // TODO: Implement update() method.
    }
}

On the other hand, ZfrForumDoctrineORM may have something that would look like this (note that the mapper here extends from EntityRepository, so that we can have a lot of operations already done for us, like find, findBy…). But as it implements our CategoryMapperInterface, this still work !

namespace ZfrForum\Repository;

use Doctrine\ORM\EntityRepository;
use ZfrForum\Entity\Category;
use ZfrForum\Mapper\CategoryMapperInterface;

class CategoryRepository extends EntityRepository implements CategoryMapperInterface
{
    /**
     * @param  Category $category
     * @return mixed
     */
    public function create(Category $category)
    {
        // TODO: Implement create() method.
    }

    /**
     * @param  Category $category
     * @return mixed
     */
    public function update(Category $category)
    {
        // TODO: Implement update() method.
    }
}

Services

Now, let’s write the service layer. As you can imagine, the service layer has a dependency with the mapper (because service layer should not directly interact with the database, this is the work of the mapper). Of course, the idea is not to use ZfrForum\Mapper\CategoryMapper or ZfrForum\Repository\CategoryRepository BUT the interface (ZfrForum\Mapper\CategoryMapperInterface), so that the implementation can switch between different implementations.

Here is CategoryService :

namespace ZfrForum\Service;

use ZfrForum\Entity\Category;
use ZfrForum\Mapper\CategoryMapperInterface;

class CategoryService
{
    /**
     * @var CategoryMapperInterface
     */
    protected $categoryMapper;

    /**
     * @param CategoryMapperInterface $categoryMapper
     */
    public function __construct(CategoryMapperInterface $categoryMapper)
    {
        $this->categoryMapper = $categoryMapper;
    }
}

Handle dependencies

CategoryService needs a mapper that implements CategoryMapperInterface. So let’s add those lines to our Module.php file to handle those dependencies (note that I’ve also take care of the ZfrForum\Mapper\CategoryMapper that needs a TableGateway object as a dependency).

public function getServiceConfig()
{
    return array(
        'factories' => array(
            'ZfrForum\Service\CategoryService' => function($serviceManager) {
                $categoryMapper = $serviceManager->get('ZfrForum\Mapper\CategoryMapperInterface');
                return new Service\CategoryService($categoryMapper);
            },

            'ZfrForum\Mapper\CategoryMapper' => function($serviceManager) {
                 $dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
                 $resultSetPrototype = new ResultSet();
                 $resultSetPrototype->setArrayObjectPrototype(new Category());

                 return new TableGateway('category', $dbAdapter, null, $resultSetPrototype);
            },
        ),
        /**
         * Abstract factories
         */
        'abstract_factories' => array(
            'ZfrForum\ServiceFactory\MapperAbstractFactory'
        )
    );
}

Of course, a new problem arise : service manager does not know what a “ZfrForum\Mapper\CategoryMapperInterface” is. We could, of course, add the following alias in our service config:

// In ZfrForum :
return array(
    'aliases' => array(
        'ZfrForum\Mapper\CategoryMapperInterface' => 'ZfrForum\Mapper\CategoryMapper'
    )
);

// In ZfrForumDoctrine :
return array(
    'aliases' => array(
        'ZfrForum\Mapper\CategoryMapperInterface' => 'ZfrForum\Repository\CategoryRepository'
    )
);

But imagine that you have 15 mappers ! You would need 15 aliases ! And of course, as different module use different implementations (ZfrForum uses ZfrForum\Mapper\CategoryMapper while ZfrForumDoctrineORM uses ZfrForum\Repository\CategoryRepository), you have to duplicate those 15 lines in both modules !

Of course, there is a better way : abstract factories. Notice in the previous code snippet the key “abstract_factories”.

Indeed, when you are calling the “get” service manager’s function, here is the different steps it will do :

  1. First, it checks if the name exists in the invokables list. If yes, it directly instantiate the object, otherwise it goes to step 2.
  2. It checks if the name is associated with one of the factories. If yes, it uses this factory to instantiate the object, other it goes to step 3.
  3. It iterates through every abstract factories, and ask the abstract factory if it can create an object with this name. If the abstact factory answers “yes”, it uses this abstract factory to create the object.

This is the mecanism we are going to use. This can work because we assume that all our mappers interfaces have the following structure : “ZfrForum\Mapper\xxxxMapperInterface”, where “xxxx” is the name of the entity that is mapped.

Therefore, our abstract factory for Zend\Db looks like this:

namespace ZfrForum\ServiceFactory;

use Zend\ServiceManager\AbstractFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class MapperAbstractFactory implements AbstractFactoryInterface
{
    /**
     * Determine if we can create a service with name
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @param                         $name
     * @param                         $requestedName
     * @return bool
     */
    public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
    {
        return (substr($requestedName, -15) === 'MapperInterface');
    }

    /**
     * Create service with name
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @param                         $name
     * @param                         $requestedName
     * @return mixed
     */
    public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
    {
        // Mapper names given are under the form "ZfrForum\Mapper\SomethingMapperInterface", so we need to
        // remove the MapperInterface part
        $parts       = explode('\\', $requestedName);
        $entityName  = substr(end($parts), 0, -15);
        $entityClass = 'ZfrForum\\Mapper\\' . $entityName . 'Mapper';

        return $serviceLocator->get($entityClass);
    }
}

As you can see, the function “canCreateServiceWithName” checks if the name ends with “MapperInterface”. If yes, we can do something and returns true. On the other hand, the function “createServiceWithName” performs some string operations in order to extract the entity name, and construct the mapper.

For Doctrine, this is even simpler, as the entity manager can construct for us the entity repository. Of course, for this to work, you have to tell to Doctrine that the repository for your Category entity is “ZfrForum\Repository\CategoryRepository” (through annotations, for example).

namespace ZfrForum\ServiceFactory;

use Zend\ServiceManager\AbstractFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class MapperAbstractFactory implements AbstractFactoryInterface
{
    /**
     * Determine if we can create a service with name
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @param                         $name
     * @param                         $requestedName
     * @return bool
     */
    public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
    {
        return (substr($requestedName, -15) === 'MapperInterface');
    }

    /**
     * Create service with name
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @param                         $name
     * @param                         $requestedName
     * @return mixed
     */
    public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
    {
        /** @var \Doctrine\ORM\EntityManager $entityManager */
        $entityManager = $serviceLocator->get('Doctrine\ORM\EntityManager');

        // Mapper names given are under the form "ZfrForum\Mapper\SomethingMapperInterface", so we need to
        // remove the MapperInterface part
        $parts       = explode('\\', $requestedName);
        $entityName  = substr(end($parts), 0, -15);
        $entityClass = 'ZfrForum\\Entity\\' . $entityName;

        return $entityManager->getRepository($entityClass);
    }
}

Conclusion

Working with interfaces allow to easily switch between adapters, while the abstract factory allows us to easily create object that have similarities (here, all the objects whose name ends with “MapperInterface”.

For instance, if we want to support Doctrine Mongo, it will be as easy as create a new module called ZfrForumDoctrineMongo, write specific implementations for every mapper, and override the abstract factories to return object document mappers instead of the default Zend\Db powered mappers.