Skip to content

Commit bcbbd15

Browse files
committed
feature #9223 [DI] Service subscribers (codedmonkey, Tim Goudriaan)
This PR was merged into the 3.4 branch. Discussion ---------- [DI] Service subscribers Fixes #7740. Couple of notes: * The page name was changed from `Service Locators` to `Service Subscribers`. Maybe `Service Subscribers & Locators` is more appropiate?? * Later on in the page it starts talking about service types as inspired by the API documentation of the `ServiceSubscriberInterface`, but this term isn't used anywhere else in the documentation. This is my first contribution to the docs so any input is appreciated. Commits ------- b1f0343 Incorporate feedback 1754ff8 Minor tweaks to service subscribers article 5a80eea Refactor service locators article to primarily describe service subscribers
2 parents 63b36c7 + b1f0343 commit bcbbd15

File tree

2 files changed

+173
-51
lines changed

2 files changed

+173
-51
lines changed

service_container/lazy_services.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Lazy Services
66

77
.. seealso::
88

9-
Another way to inject services lazily is via a :doc:`service locator </service_container/service_locators>`.
9+
Another way to inject services lazily is via a :doc:`service subscriber </service_container/service_subscribers_locators>`.
1010

1111
Why Lazy Services?
1212
------------------

service_container/service_locators.rst renamed to service_container/service_subscribers_locators.rst

Lines changed: 172 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
.. index::
2-
single: DependencyInjection; Service Locators
2+
single: DependencyInjection; Service Subscribers
33

4-
Service Locators
5-
================
4+
Service Subscribers & Locators
5+
==============================
66

77
Sometimes, a service needs access to several other services without being sure
88
that all of them will actually be used. In those cases, you may want the
@@ -77,15 +77,178 @@ However, injecting the entire container is discouraged because it gives too
7777
broad access to existing services and it hides the actual dependencies of the
7878
services.
7979

80-
**Service Locators** are intended to solve this problem by giving access to a
81-
set of predefined services while instantiating them only when actually needed.
80+
**Service Subscribers** are intended to solve this problem by giving access to a
81+
set of predefined services while instantiating them only when actually needed
82+
through a **Service Locator**, a separate lazy-loaded container.
83+
84+
Defining a Service Subscriber
85+
-----------------------------
86+
87+
First, turn ``CommandBus`` into an implementation of :class:`Symfony\\Component\\DependencyInjection\\ServiceSubscriberInterface`.
88+
Use its ``getSubscribedServices()`` method to include as many services as needed
89+
in the service subscriber and change the type hint of the container to
90+
a PSR-11 ``ContainerInterface``::
91+
92+
// src/AppBundle/CommandBus.php
93+
namespace AppBundle;
94+
95+
use AppBundle\CommandHandler\BarHandler;
96+
use AppBundle\CommandHandler\FooHandler;
97+
use Psr\Container\ContainerInterface;
98+
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
99+
100+
class CommandBus implements ServiceSubscriberInterface
101+
{
102+
private $locator;
103+
104+
public function __construct(ContainerInterface $locator)
105+
{
106+
$this->locator = $locator;
107+
}
108+
109+
public static function getSubscribedServices()
110+
{
111+
return [
112+
'AppBundle\FooCommand' => FooHandler::class,
113+
'AppBundle\BarCommand' => BarHandler::class
114+
];
115+
}
116+
117+
public function handle(Command $command)
118+
{
119+
$commandClass = get_class($command);
120+
121+
if ($this->locator->has($commandClass)) {
122+
$handler = $this->locator->get($commandClass);
123+
124+
return $handler->handle($command);
125+
}
126+
}
127+
}
128+
129+
.. tip::
130+
131+
If the container does *not* contain the subscribed services, double-check
132+
that you have :ref:`autoconfigure <services-autoconfigure>` enabled. You
133+
can also manually add the ``container.service_subscriber`` tag.
134+
135+
The injected service is an instance of :class:`Symfony\\Component\\DependencyInjection\\ServiceLocator`
136+
which implements the PSR-11 ``ContainerInterface``, but it is also a callable::
137+
138+
// ...
139+
$locateHandler = $this->locator;
140+
$handler = $locateHandler($commandClass);
141+
142+
return $handler->handle($command);
143+
144+
Including Services
145+
------------------
146+
147+
In order to add a new dependency to the service subscriber, use the
148+
``getSubscribedServices()`` method to add service types to include in the
149+
service locator::
150+
151+
use Psr\Log\LoggerInterface;
152+
153+
public static function getSubscribedServices()
154+
{
155+
return [
156+
//...
157+
LoggerInterface::class
158+
];
159+
}
160+
161+
Service types can also be keyed by a service name for internal use::
162+
163+
use Psr\Log\LoggerInterface;
164+
165+
public static function getSubscribedServices()
166+
{
167+
return [
168+
//...
169+
'logger' => LoggerInterface::class
170+
];
171+
}
172+
173+
Optional Services
174+
~~~~~~~~~~~~~~~~~
175+
176+
For optional dependencies, prepend the service type with a ``?`` to prevent
177+
errors if there's no matching service found in the service container::
178+
179+
use Psr\Log\LoggerInterface;
180+
181+
public static function getSubscribedServices()
182+
{
183+
return [
184+
//...
185+
'?'.LoggerInterface::class
186+
];
187+
}
188+
189+
.. note::
190+
191+
Make sure an optional service exists by calling ``has()`` on the service
192+
locator before calling the service itself.
193+
194+
Aliased Services
195+
~~~~~~~~~~~~~~~~
196+
197+
By default, autowiring is used to match a service type to a service from the
198+
service container. If you don't use autowiring or need to add a non-traditional
199+
service as a dependency, use the ``container.service_subscriber`` tag to map a
200+
service type to a service.
201+
202+
.. configuration-block::
203+
204+
.. code-block:: yaml
205+
206+
// app/config/services.yml
207+
services:
208+
AppBundle\CommandBus:
209+
tags:
210+
- { name: 'container.service_subscriber', key: 'logger', id: 'monolog.logger.event' }
211+
212+
.. code-block:: xml
213+
214+
<!-- app/config/services.xml -->
215+
<?xml version="1.0" encoding="UTF-8" ?>
216+
<container xmlns="http://symfony.com/schema/dic/services"
217+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
218+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
219+
220+
<services>
221+
222+
<service id="AppBundle\CommandBus">
223+
<tag name="container.service_subscriber" key="logger" id="monolog.logger.event" />
224+
</service>
225+
226+
</services>
227+
</container>
228+
229+
.. code-block:: php
230+
231+
// app/config/services.php
232+
use AppBundle\CommandBus;
233+
234+
//...
235+
236+
$container
237+
->register(CommandBus::class)
238+
->addTag('container.service_subscriber', array('key' => 'logger', 'id' => 'monolog.logger.event'))
239+
;
240+
241+
.. tip::
242+
243+
The ``key`` attribute can be omitted if the service name internally is the
244+
same as in the service container.
82245

83246
Defining a Service Locator
84247
--------------------------
85248

86-
First, define a new service for the service locator. Use its ``arguments``
87-
option to include as many services as needed to it and add the
88-
``container.service_locator`` tag to turn it into a service locator:
249+
To manually define a service locator, create a new service definition and add
250+
the ``container.service_locator`` tag to it. Use its ``arguments`` option to
251+
include as many services as needed in it.
89252

90253
.. configuration-block::
91254

@@ -144,7 +307,7 @@ option to include as many services as needed to it and add the
144307
The services defined in the service locator argument must include keys,
145308
which later become their unique identifiers inside the locator.
146309

147-
Now you can use the service locator injecting it in any other service:
310+
Now you can use the service locator by injecting it in any other service:
148311

149312
.. configuration-block::
150313

@@ -188,45 +351,4 @@ Now you can use the service locator injecting it in any other service:
188351
If the service locator is not intended to be used by multiple services, it's
189352
better to create and inject it as an anonymous service.
190353

191-
Usage
192-
-----
193-
194-
Back to the previous ``CommandBus`` example, it looks like this when using the
195-
service locator::
196-
197-
// ...
198-
use Psr\Container\ContainerInterface;
199-
200-
class CommandBus
201-
{
202-
/**
203-
* @var ContainerInterface
204-
*/
205-
private $handlerLocator;
206-
207-
// ...
208-
209-
public function handle(Command $command)
210-
{
211-
$commandClass = get_class($command);
212-
213-
if (!$this->handlerLocator->has($commandClass)) {
214-
return;
215-
}
216-
217-
$handler = $this->handlerLocator->get($commandClass);
218-
219-
return $handler->handle($command);
220-
}
221-
}
222-
223-
The injected service is an instance of :class:`Symfony\\Component\\DependencyInjection\\ServiceLocator`
224-
which implements the PSR-11 ``ContainerInterface``, but it is also a callable::
225-
226-
// ...
227-
$locateHandler = $this->handlerLocator;
228-
$handler = $locateHandler($commandClass);
229-
230-
return $handler->handle($command);
231-
232354
.. _`Command pattern`: https://en.wikipedia.org/wiki/Command_pattern

0 commit comments

Comments
 (0)