Skip to content

Commit ca9f215

Browse files
committed
minor #7458 [DI] Add section about service locators (chalasr, javiereguiluz)
This PR was merged into the master branch. Discussion ---------- [DI] Add section about service locators Adds documentation for symfony/symfony#21553 and symfony/symfony#22024. Any suggestion will be much appreciated, as usual. Commits ------- fa19770 Fix service locator declaration f5e4942 Rewords 5efacd0 [DI] Add section about Service Locators
2 parents 0e26110 + fa19770 commit ca9f215

File tree

1 file changed

+226
-0
lines changed

1 file changed

+226
-0
lines changed
+226
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
.. index::
2+
single: DependencyInjection; Service Locators
3+
4+
Service Locators
5+
================
6+
7+
Sometimes, a service needs access to several other services without being sure
8+
that all of them will actually be used. In those cases, you may want the
9+
instantiation of the services to be lazy. However, that's not possible using
10+
the explicit dependency injection since services are not all meant to
11+
be ``lazy`` (see :doc:`/service_container/lazy_services`).
12+
13+
A real-world example are applications that implement the `Command pattern`_
14+
using a CommandBus to map command handlers by Command class names and use them
15+
to handle their respective command when it is asked for::
16+
17+
// ...
18+
class CommandBus
19+
{
20+
/**
21+
* @var CommandHandler[]
22+
*/
23+
private $handlerMap;
24+
25+
public function __construct(array $handlerMap)
26+
{
27+
$this->handlerMap = $handlerMap;
28+
}
29+
30+
public function handle(Command $command)
31+
{
32+
$commandClass = get_class($command)
33+
34+
if (!isset($this->handlerMap[$commandClass])) {
35+
return;
36+
}
37+
38+
return $this->handlerMap[$commandClass]->handle($command);
39+
}
40+
}
41+
42+
// ...
43+
$commandBus->handle(new FooCommand());
44+
45+
Considering that only one command is handled at a time, instantiating all the
46+
other command handlers is unnecessary. A possible solution to lazy-load the
47+
handlers could be to inject the whole dependency injection container::
48+
49+
use Symfony\Component\DependencyInjection\ContainerInterface;
50+
51+
class CommandBus
52+
{
53+
private $container;
54+
55+
public function __construct(ContainerInterface $container)
56+
{
57+
$this->container = $container;
58+
}
59+
60+
public function handle(Command $command)
61+
{
62+
$commandClass = get_class($command)
63+
64+
if ($this->container->has($commandClass)) {
65+
$handler = $this->container->get($commandClass);
66+
67+
return $handler->handle($command);
68+
}
69+
}
70+
}
71+
72+
However, injecting the entire container is discouraged because it gives too
73+
broad access to existing services and it hides the actual dependencies of the
74+
services.
75+
76+
**Service Locators** are intended to solve this problem by giving access to a
77+
set of predefined services while instantiating them only when actually needed.
78+
79+
Defining a Service Locator
80+
--------------------------
81+
82+
First, define a new service for the service locator. Use its ``arguments``
83+
option to include as many services as needed to it and add the
84+
``container.service_locator`` tag to turn it into a service locator:
85+
86+
.. configuration-block::
87+
88+
.. code-block:: yaml
89+
90+
services:
91+
92+
app.command_handler_locator:
93+
class: Symfony\Component\DependencyInjection\ServiceLocator
94+
tags: ['container.service_locator']
95+
arguments:
96+
-
97+
AppBundle\FooCommand: '@app.command_handler.foo'
98+
AppBundle\BarCommand: '@app.command_handler.bar'
99+
100+
.. code-block:: xml
101+
102+
<?xml version="1.0" encoding="UTF-8" ?>
103+
<container xmlns="http://symfony.com/schema/dic/services"
104+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
105+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
106+
107+
<services>
108+
109+
<service id="app.command_handler_locator" class="Symfony\Component\DependencyInjection\ServiceLocator">
110+
<argument type="collection">
111+
<argument key="AppBundle\FooCommand" type="service" id="app.command_handler.foo" />
112+
<argument key="AppBundle\BarCommand" type="service" id="app.command_handler.bar" />
113+
</argument>
114+
<tag name="container.service_locator" />
115+
</service>
116+
117+
</services>
118+
</container>
119+
120+
.. code-block:: php
121+
122+
use Symfony\Component\DependencyInjection\ServiceLocator;
123+
use Symfony\Component\DependencyInjection\Reference;
124+
125+
//...
126+
127+
$container
128+
->register('app.command_handler_locator', ServiceLocator::class)
129+
->addTag('container.service_locator')
130+
->setArguments(array(array(
131+
'AppBundle\FooCommand' => new Reference('app.command_handler.foo'),
132+
'AppBundle\BarCommand' => new Reference('app.command_handler.bar'),
133+
)))
134+
;
135+
136+
.. note::
137+
138+
The services defined in the service locator argument must include keys,
139+
which later become their unique identifiers inside the locator.
140+
141+
Now you can use the service locator injecting it in any other service:
142+
143+
.. configuration-block::
144+
145+
.. code-block:: yaml
146+
147+
services:
148+
149+
AppBundle\CommandBus:
150+
arguments: ['@app.command_handler_locator']
151+
152+
.. code-block:: xml
153+
154+
<?xml version="1.0" encoding="UTF-8" ?>
155+
<container xmlns="http://symfony.com/schema/dic/services"
156+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
157+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
158+
159+
<services>
160+
161+
<service id="AppBundle\CommandBus">
162+
<argument type="service" id="app.command_handler.locator" />
163+
</service>
164+
165+
</services>
166+
</container>
167+
168+
.. code-block:: php
169+
170+
use AppBundle\CommandBus;
171+
use Symfony\Component\DependencyInjection\Reference;
172+
173+
//...
174+
175+
$container
176+
->register(CommandBus::class)
177+
->setArguments(array(new Reference('app.command_handler_locator')))
178+
;
179+
180+
.. tip::
181+
182+
If the service locator is not intended to be used by multiple services, it's
183+
better to create and inject it as an anonymous service.
184+
185+
Usage
186+
-----
187+
188+
Back to the previous CommandBus example, it looks like this when using the
189+
service locator::
190+
191+
// ...
192+
use Psr\Container\ContainerInterface;
193+
194+
class CommandBus
195+
{
196+
/**
197+
* @var ContainerInterface
198+
*/
199+
private $handlerLocator;
200+
201+
// ...
202+
203+
public function handle(Command $command)
204+
{
205+
$commandClass = get_class($command);
206+
207+
if (!$this->handlerLocator->has($commandClass)) {
208+
return;
209+
}
210+
211+
$handler = $this->handlerLocator->get($commandClass);
212+
213+
return $handler->handle($command);
214+
}
215+
}
216+
217+
The injected service is an instance of :class:`Symfony\\Component\\DependencyInjection\\ServiceLocator`
218+
which implements the PSR-11 ``ContainerInterface``, but it is also a callable::
219+
220+
// ...
221+
$locateHandler = $this->handlerLocator;
222+
$handler = $locateHandler($commandClass);
223+
224+
return $handler->handle($command);
225+
226+
.. _`Command pattern`: https://en.wikipedia.org/wiki/Command_pattern

0 commit comments

Comments
 (0)