Robin Chalas Nicolas Grekas
Contributed by Robin Chalas and Nicolas Grekas in #21553 and #22024

In Symfony applications, some services need access to several other services although some of them will not be actually used (e.g. the FirewallMap class). Instantiating all those unused services is useless, but it's not possible to turn them into lazy services using explicit dependency injection.

The traditional solution in those cases was to inject the entire service container to get only the services really needed. However, this is not recommended because it gives services a too broad access to the rest of the application and it hides the actual dependencies of the services.

Service locators are a design pattern that "encapsulate the processes involved in obtaining a service [...] using a central registry known as the service locator". This pattern is often discouraged, but it's useful in these cases and it's way better than injecting the entire service container.

Consider a CommandBus class that maps commands and their handlers. This class handles only one command at a time, so it's useless to instantiate all of them. First, define a service locator service with the new container.service_locator tag and add all the commands as arguments:

1
2
3
4
5
6
7
8
9
# app/config/services.yml
services:
    app.command_handler_locator:
        class: Symfony\Component\DependencyInjection\ServiceLocator
        tags: ['container.service_locator']
        arguments:
            -
                AppBundle\FooCommand: '@app.command_handler.foo'
                AppBundle\BarCommand: '@app.command_handler.bar'

Then, inject the service locator into the service defined for the command bus:

1
2
3
4
# app/config/services.yml
services:
    AppBundle\CommandBus:
        arguments: ['@app.command_handler_locator']

The injected service locator is an instance of Symfony\Component\DependencyInjection\ServiceLocator. This class implements the PSR-11 ContainerInterface, which includes the has() and get() methods to check and get services from the locator:

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
// ...
use Psr\Container\ContainerInterface;

class CommandBus
{
    /** @var ContainerInterface */
    private $handlerLocator;

    // ...

    public function handle(Command $command)
    {
        $commandClass = get_class($command);

        // check if some service is included in the service locator
        if (!$this->handlerLocator->has($commandClass)) {
            return;
        }

        // get the service from the service locator (and instantiate it)
        $handler = $this->handlerLocator->get($commandClass);

        return $handler->handle($command);
    }
}
Published in #Living on the edge