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);
}
}
It's usually always possible to do that: what's the encountered edge case?
Also, make sure to throw an exception in that command bus 👍
What are the benefits compared to using ContainerAwareCommand?
In the case of simple dependency call, you can define a service who call the specific service needed, that's easy and autowiring is not needed anymore.
I've always said that allowing to access the service container inside the services.yml file is a bad idea, I prefer to use private service to restrict the call capacities of the controllers and type the service that I need from one service to another.
For me, it seems that this solution allow a factory service who return many services, who's a bad idea (because well, the service container allow the same approach in some circumstances), in my opinion, the factory service contains the whole services injected declarations and this way, all the service who need this factory service has access to the method like if the service was injected manually by the services.yml.
For the special case of a command, I rather prefer to inject only the service that I need directly in the services.yml definition rather than using a "factory" who inject for me :/
@Max the docs about Service Locators have been published and they explain things in more detail: https://symfony.com/doc/master/service_container/service_locators.html
Do you think it is feasible to pass a lambda, which has the service locator bound, so when the lambda gets executed it will return the service object? If not executed then nothing happens all all is lazy.
@Max ContainerAware = injecting the DIC, a DIC is not meant to be injected into services but to inject things into them. A service locator is meant to be injected, and the class scope is identifiable through the services its locator has access to.
@Loulier This feature solves a very specific need that your domain should not have. Yes, a locator gives access to several services, but they are all identified.