Handling Multiple Entity Managers in Doctrine, the Smart Way!

Intro

So here is the situation: your Symfony2 powered website supports a bunch of countries/language, each country requires a specific set of data to be served, and to top it all up, you are using separate databases for each country and the databases are handled by doctrine.

If you are faced by such a situation, you will feel the pain of managing locales and switching between different entity managers in your code.

Steps

  • Doctrine configuration:

You have a doctrine configuration with multiple Database connections and Entity Managers as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
doctrine:
    dbal:
        connections:
            us:
                driver:   "%database_driver_us%"
                host:     "%database_host_us%"
                port:     "%database_port_us%"
                dbname:   "%database_name_us%"
                user:     "%database_user_us%"
                password: "%database_password_us%"
                charset:  UTF8
                mapping_types:
                    enum: string
            uk:
                driver:   "%database_driver_uk%"
                host:     "%database_host_uk%"
                port:     "%database_port_uk%"
                dbname:   "%database_name_uk%"
                user:     "%database_user_uk%"
                password: "%database_password_uk%"
                charset:  UTF8
                mapping_types:
                    enum: string

    orm:
        auto_generate_proxy_classes: false
        entity_managers:
          us:
              connection: us
              mappings:
                  SomeBundle: ~
          uk:
              connection: uk
              mappings:
                  SomeBundle: ~
  • URL Format

Choose domain.com/en-us

  • Correctly set you routing.yml to have the locale on the root of the url

on the root routing.yml in app/config/routing.yml set the following on your bundles

1
2
3
4
5
6
some_bundle:
    resource: "@SomeBundle/Resources/config/routing.yml"
    prefix:   /{language}-{country}/
    requirements:
      language: en|lang2|lang3
      country:  us|country2|country3

At this point, you limited the access to your bundle resources to a specific set of language, country (locales) combinations.

  • Create the LocaleManager

create a LocaleManager, where your target EntityManager, country and language will reside.

in services.yml:

1
2
locale_manager:
    class: Company\SomeBundle\Service\LocaleManager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php

...

class LocaleManager
{
    protected $em;

    protected $country;

    protected $language;

    public function setEm(EntityManager $em)
    {
        $this->em = $em;
    }

    public function getEm()
    {
        return $this->em;
    }

    public function setCountry($country)
    {
        $this->country = $country;
    }

    public function getCountry()
    {
        return $this->country;
    }

    public function setLanguage($language)
    {
        $this->language = $language;
    }

    public function getLanguage()
    {
        return $this->language;
    }
}
  • Create the RequestListener

create a request listener in your bundle. The preferred directory is SomeBundle/Listener/RequestListener

in services.yml:

1
2
3
4
5
6
7
8
kernel.listener.request_listener:
    class: Company\SomeBundle\Listener\RequestListener
    arguments:
      localeManager: @locale_manager
      doctrine:      @doctrine
      router:        @router
    tags:
      - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

...

class RequestListener
{
    protected $localeManager;

    protected $doctrine;

    protected $router;

    public function __construct(LocaleManager $localeManager, Doctrine $doctrine, Router $router)
    {
        $this->localeManager = $localeManager;
        $this->doctrine      = $doctrine;
        $this->router        = $router;
    }

    public function onKernelRequest(GetResponseEvent $event)
    {
        $country  = $event->getRequest()->get('country');
        $language = $event->getRequest()->get('language');

        $this->localeManager->setEm($this->doctrine->getManager($country));
        $this->localeManager->setCountry($country);
        $this->localeManager->setLanguage($language):
    }
}

Use it!

Now, you have @locale_manager, which has the current request’s EntityManager, country and language to be used transparently all over your codebase!

1
2
3
4
some_service:
    class: path\to\class
    arguments:
        localeManager: @locale_manager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

...

class SomeService
{
    protected $localeManager;

    public function __construct(LocaleManager $localeManager)
    {
        $this->localeManager = $localeManager;
    }

    public function getData()
    {
        return $this->requestManager->getEm()->getRepository('SomeBundle:SomeEntity')->findOneByProperty('property');
    }
}

which could have been somehting like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

...

class SomeService
{
    protected $doctrine;

    public function __construct(Doctrine $doctrine)
    {
        $this->doctrine = $doctrine;
    }

    public function getData($country)
    {
        return $this->doctrine->getManager($country)->getRepository('SomeBundle:SomeEntity')->findOneByProperty('property');
    }
}

Country and Language information can also be useful when you want to do translations, rendering templates, etc..

By the way, instead of storing $country and $language as strings in the LocaleManager, you can create a country and language objects, where you can store country/language specific information to be used transparently across your codebase!

Have fun!

Comments