New in Symfony 3.3: Service locators
May 4, 2017 • Published by Javier Eguiluz
Warning: This post is about an unsupported Symfony version. Some of this information may be out of date. Read the most recent Symfony Docs.
Contributed by
Robin Chalas
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);
}
}
Help the Symfony project!
As with any Open-Source project, contributing code or documentation is the most common way to help, but we also have a wide range of sponsoring opportunities.
Comments are closed.
To ensure that comments stay relevant, they are closed for old posts.
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 👍
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 :/
@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.