PHP & Symfony About PHP and Symfony2 development

Inject the ManagerRegistry instead of the EntityManager

Posted on by Matthias Noback

Please also read my next article about injecting entity managers.

Or: how to make your persistence-related code more reusable

You probably know that you should trim your controllers as much as possible. This usually means extracting some classes from the semi-procedural code inside your controller class. You create services for the extracted classes and inject the necessary dependencies into them. You can no longer use the convenient $this->getDoctrine()->getManager(). Instead you inject the doctrine.orm.default_entity_manager service into every service that needs the EntityManager to persist data in your relational database:

namespace AcmeCustomerBundle\Entity;

use Doctrine\ORM\EntityManager;

class CustomerManager
{
    private $entityManager;

    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function createCustomer(Customer $customer)
    {
        $this->entityManager->persist($customer);
        $this->entityManager->flush();
    }
}

When you don't intend to reuse this code in other projects, or share it as open source software: no problem! But when you do, consider the following. The DoctrineBundle offers a way to set up multiple entity managers and database connections. This way you can persist different types of entities in your project in different database. Though I guess most projects use just one entity manager and one database connection, when you want your code to be truly reusable, you need to consider the possibility that it will be used in a project with multiple entity managers.

This is what the Doctrine configuration of a Symfony2 project with multiple entity managers looks like (copied from the official documentation):

doctrine:
    dbal:
        default_connection: default
        connections:
            default:
                driver:   "%database_driver%"
                dbname:   "%database_name%"
                ...
            customer:
                driver:   "%database_driver2%"
                dbname:   "%database_name2%"
                ...
    orm:
        default_entity_manager: default
        entity_managers:
            default:
                connection: default
                mappings:
                    AcmeDemoBundle:  ~
                    AcmeStoreBundle: ~
            customer:
                connection: customer
                mappings:
                    AcmeCustomerBundle: ~

This particular configuration defines two different database connections used by two different entity managers which manage entities from different bundles.

Now let's say you release the CustomerManager class from the example above as part of the open source bundle AcmeCustomerBundle. You add the following service definition in Acme/CustomerBundle/Resources/config/services.yml:

services:
    acme_customer.customer_manager:
        class: Acme\CustomerBundle\Doctrine\CustomerManager
        arguments:
            - @doctrine.orm.default_entity_manager

However, when this bundle is installed in a project with the Doctrine configuration above, the entities in the AcmeCustomerBundle will be managed by the customer entity manager (which is available as the doctrine.orm.customer_entity_manager service), not by the default entity manager. This means that the entity manager that is being injected in the CustomerManager class is the wrong one: it is not capable at all to manage the Customer entity from the AcmeCustomerBundle.

Since you can not know in advance which entity manager the user of your bundle wants to use, you need to remove the hard dependency on the default entity manager. Instead you should depend on the ManagerRegistry, which knows all about the available entity managers in a project and is able to find the right entity manager for a given entity class.

In fact, when you use $this->getDoctrine() in your controller, you already receive an instance of the ManagerRegistry interface. When you call its method getManager(), it returns the default entity manager.

The ManagerRegistry also has a method getManagerForClass($class) which tries to find an entity manager which is able to manage entities of the given class. When we ask it to find the entity manager for the Acme\CustomerBundle\Entity\Customer class, it returns the customer entity manager. When one of the other entity classes is provided, it returns the default entity manager.

In practice this means that classes that need an entity manager, should not get an EntityManager instance injected, but instead should receive an ManagerRegistry instance:

use Doctrine\Common\Persistence\ManagerRegistry;

class CustomerManager
{
    private $managerRegistry;

    public function __construct(ManagerRegistry $managerRegistry)
    {
        $this->managerRegistry = $managerRegistry;
    }

    public function createCustomer(Customer $customer)
    {
        // retrieve the right entity manager for this type of entity
        $entityManager = $managerRegistry->getManagerForClass(get_class($customer));

        $entityManager->persist($customer);
        $entityManager->flush();
    }
}

Finally the service definition of CustomerManager needs to be modified: the constructor argument should not be doctrine.orm.default_entity_manager anymore, instead it should be doctrine, which is the ManagerRegistry containing all the entity managers known inside the current application.

services:
    acme_customer.customer_manager:
        class: Acme\CustomerBundle\Doctrine\CustomerManager
        arguments:
            - @doctrine

We have accomplished maximum reusability since the CustomerManager can now be used in projects with multiple entity managers.

Bonus: support for different object managers

As you may have noticed, the ManagerRegistry interface was imported from the Doctrine Common library. Its interface says that the getManager*() methods return an instance of ObjectManager (which is also part of the Doctrine Common library). All the object managers from Doctrine persistence libraries like Doctrine ORM, Doctrine MongoDB ODM, Doctrine CouchDB ODM, etc. implement this ObjectManager interface, which itself offers methods like persist() and flush(). This means that once you make your classes depend on the ManagerRegistry interface instead of the EntityManager class, you have made them reusable in many other contexts than just the context of a project that uses Doctrine ORM for persistence.

Take a look at the FOSUserBundle and see how they solved the problem of different persistence libraries. Also take a look at the interfaces and base classes in the Doctrine Common Persistence sub-library. This will make you aware of all the "cheap" possibilities of making your persistence-related code even more reusable.

Categories: PHP Symfony

Tags: Doctrine ORM persistence reusability

Comments: Comments