mirror of
https://github.com/symfony/symfony-docs.git
synced 2026-03-24 00:32:14 +01:00
663 lines
21 KiB
ReStructuredText
663 lines
21 KiB
ReStructuredText
.. index::
|
|
single: DependencyInjection; Service Subscribers
|
|
|
|
.. _service-locators:
|
|
|
|
Service Subscribers & Locators
|
|
==============================
|
|
|
|
Sometimes, a service needs access to several other services without being sure
|
|
that all of them will actually be used. In those cases, you may want the
|
|
instantiation of the services to be lazy. However, that's not possible using
|
|
the explicit dependency injection since services are not all meant to
|
|
be ``lazy`` (see :doc:`/service_container/lazy_services`).
|
|
|
|
This can typically be the case in your controllers, where you may inject several
|
|
services in the constructor, but the action called only uses some of them.
|
|
Another example are applications that implement the `Command pattern`_
|
|
using a CommandBus to map command handlers by Command class names and use them
|
|
to handle their respective command when it is asked for::
|
|
|
|
// src/CommandBus.php
|
|
namespace App;
|
|
|
|
// ...
|
|
class CommandBus
|
|
{
|
|
/**
|
|
* @var CommandHandler[]
|
|
*/
|
|
private $handlerMap;
|
|
|
|
public function __construct(array $handlerMap)
|
|
{
|
|
$this->handlerMap = $handlerMap;
|
|
}
|
|
|
|
public function handle(Command $command)
|
|
{
|
|
$commandClass = get_class($command);
|
|
|
|
if (!isset($this->handlerMap[$commandClass])) {
|
|
return;
|
|
}
|
|
|
|
return $this->handlerMap[$commandClass]->handle($command);
|
|
}
|
|
}
|
|
|
|
// ...
|
|
$commandBus->handle(new FooCommand());
|
|
|
|
Considering that only one command is handled at a time, instantiating all the
|
|
other command handlers is unnecessary. A possible solution to lazy-load the
|
|
handlers could be to inject the main dependency injection container.
|
|
|
|
However, injecting the entire container is discouraged because it gives too
|
|
broad access to existing services and it hides the actual dependencies of the
|
|
services. Doing so also requires services to be made public, which isn't the
|
|
case by default in Symfony applications.
|
|
|
|
**Service Subscribers** are intended to solve this problem by giving access to a
|
|
set of predefined services while instantiating them only when actually needed
|
|
through a **Service Locator**, a separate lazy-loaded container.
|
|
|
|
Defining a Service Subscriber
|
|
-----------------------------
|
|
|
|
First, turn ``CommandBus`` into an implementation of :class:`Symfony\\Component\\DependencyInjection\\ServiceSubscriberInterface`.
|
|
Use its ``getSubscribedServices()`` method to include as many services as needed
|
|
in the service subscriber and change the type hint of the container to
|
|
a PSR-11 ``ContainerInterface``::
|
|
|
|
// src/CommandBus.php
|
|
namespace App;
|
|
|
|
use App\CommandHandler\BarHandler;
|
|
use App\CommandHandler\FooHandler;
|
|
use Psr\Container\ContainerInterface;
|
|
use Symfony\Contracts\Service\ServiceSubscriberInterface;
|
|
|
|
class CommandBus implements ServiceSubscriberInterface
|
|
{
|
|
private $locator;
|
|
|
|
public function __construct(ContainerInterface $locator)
|
|
{
|
|
$this->locator = $locator;
|
|
}
|
|
|
|
public static function getSubscribedServices()
|
|
{
|
|
return [
|
|
'App\FooCommand' => FooHandler::class,
|
|
'App\BarCommand' => BarHandler::class,
|
|
];
|
|
}
|
|
|
|
public function handle(Command $command)
|
|
{
|
|
$commandClass = get_class($command);
|
|
|
|
if ($this->locator->has($commandClass)) {
|
|
$handler = $this->locator->get($commandClass);
|
|
|
|
return $handler->handle($command);
|
|
}
|
|
}
|
|
}
|
|
|
|
.. tip::
|
|
|
|
If the container does *not* contain the subscribed services, double-check
|
|
that you have :ref:`autoconfigure <services-autoconfigure>` enabled. You
|
|
can also manually add the ``container.service_subscriber`` tag.
|
|
|
|
The injected service is an instance of :class:`Symfony\\Component\\DependencyInjection\\ServiceLocator`
|
|
which implements the PSR-11 ``ContainerInterface``, but it is also a callable::
|
|
|
|
// ...
|
|
$handler = ($this->locator)($commandClass);
|
|
|
|
return $handler->handle($command);
|
|
|
|
Including Services
|
|
------------------
|
|
|
|
In order to add a new dependency to the service subscriber, use the
|
|
``getSubscribedServices()`` method to add service types to include in the
|
|
service locator::
|
|
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
public static function getSubscribedServices()
|
|
{
|
|
return [
|
|
// ...
|
|
LoggerInterface::class,
|
|
];
|
|
}
|
|
|
|
Service types can also be keyed by a service name for internal use::
|
|
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
public static function getSubscribedServices()
|
|
{
|
|
return [
|
|
// ...
|
|
'logger' => LoggerInterface::class,
|
|
];
|
|
}
|
|
|
|
When extending a class that also implements ``ServiceSubscriberInterface``,
|
|
it's your responsibility to call the parent when overriding the method. This
|
|
typically happens when extending ``AbstractController``::
|
|
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
|
|
class MyController extends AbstractController
|
|
{
|
|
public static function getSubscribedServices()
|
|
{
|
|
return array_merge(parent::getSubscribedServices(), [
|
|
// ...
|
|
'logger' => LoggerInterface::class,
|
|
]);
|
|
}
|
|
}
|
|
|
|
Optional Services
|
|
~~~~~~~~~~~~~~~~~
|
|
|
|
For optional dependencies, prepend the service type with a ``?`` to prevent
|
|
errors if there's no matching service found in the service container::
|
|
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
public static function getSubscribedServices()
|
|
{
|
|
return [
|
|
// ...
|
|
'?'.LoggerInterface::class,
|
|
];
|
|
}
|
|
|
|
.. note::
|
|
|
|
Make sure an optional service exists by calling ``has()`` on the service
|
|
locator before calling the service itself.
|
|
|
|
Aliased Services
|
|
~~~~~~~~~~~~~~~~
|
|
|
|
By default, autowiring is used to match a service type to a service from the
|
|
service container. If you don't use autowiring or need to add a non-traditional
|
|
service as a dependency, use the ``container.service_subscriber`` tag to map a
|
|
service type to a service.
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# config/services.yaml
|
|
services:
|
|
App\CommandBus:
|
|
tags:
|
|
- { name: 'container.service_subscriber', key: 'logger', id: 'monolog.logger.event' }
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- config/services.xml -->
|
|
<?xml version="1.0" encoding="UTF-8" ?>
|
|
<container xmlns="http://symfony.com/schema/dic/services"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
|
|
|
|
<services>
|
|
|
|
<service id="App\CommandBus">
|
|
<tag name="container.service_subscriber" key="logger" id="monolog.logger.event"/>
|
|
</service>
|
|
|
|
</services>
|
|
</container>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/services.php
|
|
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
|
|
|
use App\CommandBus;
|
|
|
|
return function(ContainerConfigurator $configurator) {
|
|
$services = $configurator->services();
|
|
|
|
$services->set(CommandBus::class)
|
|
->tag('container.service_subscriber', ['key' => 'logger', 'id' => 'monolog.logger.event']);
|
|
};
|
|
|
|
.. tip::
|
|
|
|
The ``key`` attribute can be omitted if the service name internally is the
|
|
same as in the service container.
|
|
|
|
Defining a Service Locator
|
|
--------------------------
|
|
|
|
To manually define a service locator, create a new service definition and add
|
|
the ``container.service_locator`` tag to it. Use the first argument of the
|
|
service definition to pass a collection of services to the service locator:
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# config/services.yaml
|
|
services:
|
|
app.command_handler_locator:
|
|
class: Symfony\Component\DependencyInjection\ServiceLocator
|
|
arguments:
|
|
-
|
|
App\FooCommand: '@app.command_handler.foo'
|
|
App\BarCommand: '@app.command_handler.bar'
|
|
# if you are not using the default service autoconfiguration,
|
|
# add the following tag to the service definition:
|
|
# tags: ['container.service_locator']
|
|
|
|
# if the element has no key, the ID of the original service is used
|
|
app.another_command_handler_locator:
|
|
class: Symfony\Component\DependencyInjection\ServiceLocator
|
|
arguments:
|
|
-
|
|
- '@app.command_handler.baz'
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- config/services.xml -->
|
|
<?xml version="1.0" encoding="UTF-8" ?>
|
|
<container xmlns="http://symfony.com/schema/dic/services"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
|
|
|
|
<services>
|
|
|
|
<service id="app.command_handler_locator" class="Symfony\Component\DependencyInjection\ServiceLocator">
|
|
<argument type="collection">
|
|
<argument key="App\FooCommand" type="service" id="app.command_handler.foo"/>
|
|
<argument key="App\BarCommand" type="service" id="app.command_handler.bar"/>
|
|
<!-- if the element has no key, the ID of the original service is used -->
|
|
<argument type="service" id="app.command_handler.baz"/>
|
|
</argument>
|
|
<!--
|
|
if you are not using the default service autoconfiguration,
|
|
add the following tag to the service definition:
|
|
<tag name="container.service_locator"/>
|
|
-->
|
|
</service>
|
|
|
|
</services>
|
|
</container>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/services.php
|
|
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
|
|
|
use Symfony\Component\DependencyInjection\ServiceLocator;
|
|
|
|
return function(ContainerConfigurator $configurator) {
|
|
$services = $configurator->services();
|
|
|
|
$services->set('app.command_handler_locator', ServiceLocator::class)
|
|
->args([[
|
|
'App\FooCommand' => ref('app.command_handler.foo'),
|
|
'App\BarCommand' => ref('app.command_handler.bar'),
|
|
]])
|
|
// if you are not using the default service autoconfiguration,
|
|
// add the following tag to the service definition:
|
|
// ->tag('container.service_locator')
|
|
;
|
|
|
|
// if the element has no key, the ID of the original service is used
|
|
$services->set('app.another_command_handler_locator', ServiceLocator::class)
|
|
->args([[
|
|
ref('app.command_handler.baz'),
|
|
]])
|
|
;
|
|
};
|
|
|
|
.. versionadded:: 4.1
|
|
|
|
The service locator autoconfiguration was introduced in Symfony 4.1. In
|
|
previous Symfony versions you always needed to add the
|
|
``container.service_locator`` tag explicitly.
|
|
|
|
.. versionadded:: 4.2
|
|
|
|
The ability to add services without specifying their id was introduced in
|
|
Symfony 4.2.
|
|
|
|
.. note::
|
|
|
|
The services defined in the service locator argument must include keys,
|
|
which later become their unique identifiers inside the locator.
|
|
|
|
Now you can use the service locator by injecting it in any other service:
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# config/services.yaml
|
|
services:
|
|
App\CommandBus:
|
|
arguments: ['@app.command_handler_locator']
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- config/services.xml -->
|
|
<?xml version="1.0" encoding="UTF-8" ?>
|
|
<container xmlns="http://symfony.com/schema/dic/services"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
|
|
|
|
<services>
|
|
|
|
<service id="App\CommandBus">
|
|
<argument type="service" id="app.command_handler_locator"/>
|
|
</service>
|
|
|
|
</services>
|
|
</container>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/services.php
|
|
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
|
|
|
use App\CommandBus;
|
|
|
|
return function(ContainerConfigurator $configurator) {
|
|
$services = $configurator->services();
|
|
|
|
$services->set(CommandBus::class)
|
|
->args([ref('app.command_handler_locator')]);
|
|
};
|
|
|
|
In :doc:`compiler passes </service_container/compiler_passes>` it's recommended
|
|
to use the :method:`Symfony\\Component\\DependencyInjection\\Compiler\\ServiceLocatorTagPass::register`
|
|
method to create the service locators. This will save you some boilerplate and
|
|
will share identical locators among all the services referencing them::
|
|
|
|
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
|
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
|
use Symfony\Component\DependencyInjection\Reference;
|
|
|
|
public function process(ContainerBuilder $container)
|
|
{
|
|
// ...
|
|
|
|
$locateableServices = [
|
|
// ...
|
|
'logger' => new Reference('logger'),
|
|
];
|
|
|
|
$myService->addArgument(ServiceLocatorTagPass::register($container, $locateableServices));
|
|
}
|
|
|
|
Indexing the Collection of Services
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Services passed to the service locator can define their own index using an
|
|
arbitrary attribute whose name is defined as ``index_by`` in the service locator.
|
|
|
|
In the following example, the ``App\Handler\HandlerCollection`` locator receives
|
|
all services tagged with ``app.handler`` and they are indexed using the value
|
|
of the ``key`` tag attribute (as defined in the ``index_by`` locator option):
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# config/services.yaml
|
|
services:
|
|
App\Handler\One:
|
|
tags:
|
|
- { name: 'app.handler', key: 'handler_one' }
|
|
|
|
App\Handler\Two:
|
|
tags:
|
|
- { name: 'app.handler', key: 'handler_two' }
|
|
|
|
App\HandlerCollection:
|
|
# inject all services tagged with app.handler as first argument
|
|
arguments: [!tagged_locator { tag: 'app.handler', index_by: 'key' }]
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- config/services.xml -->
|
|
<?xml version="1.0" encoding="UTF-8" ?>
|
|
<container xmlns="http://symfony.com/schema/dic/services"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="http://symfony.com/schema/dic/services
|
|
https://symfony.com/schema/dic/services/services-1.0.xsd">
|
|
|
|
<services>
|
|
<service id="App\Handler\One">
|
|
<tag name="app.handler" key="handler_one"/>
|
|
</service>
|
|
|
|
<service id="App\Handler\Two">
|
|
<tag name="app.handler" key="handler_two"/>
|
|
</service>
|
|
|
|
<service id="App\HandlerCollection">
|
|
<!-- inject all services tagged with app.handler as first argument -->
|
|
<argument type="tagged_locator" tag="app.handler" index-by="key"/>
|
|
</service>
|
|
</services>
|
|
</container>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/services.php
|
|
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
|
|
|
return function(ContainerConfigurator $configurator) {
|
|
$services = $configurator->services();
|
|
|
|
$services->set(App\Handler\One::class)
|
|
->tag('app.handler', ['key' => 'handler_one'])
|
|
;
|
|
|
|
$services->set(App\Handler\Two::class)
|
|
->tag('app.handler', ['key' => 'handler_two'])
|
|
;
|
|
|
|
$services->set(App\Handler\HandlerCollection::class)
|
|
// inject all services tagged with app.handler as first argument
|
|
->args([tagged_locator('app.handler', 'key')])
|
|
;
|
|
};
|
|
|
|
Inside this locator you can retrieve services by index using the value of the
|
|
``key`` attribute. For example, to get the ``App\Handler\Two`` service::
|
|
|
|
// src/Handler/HandlerCollection.php
|
|
namespace App\Handler;
|
|
|
|
use Symfony\Component\DependencyInjection\ServiceLocator;
|
|
|
|
class HandlerCollection
|
|
{
|
|
public function __construct(ServiceLocator $locator)
|
|
{
|
|
$handlerTwo = $locator->get('handler_two');
|
|
}
|
|
|
|
// ...
|
|
}
|
|
|
|
Instead of defining the index in the service definition, you can return its
|
|
value in a method called ``getDefaultIndexName()`` inside the class associated
|
|
to the service::
|
|
|
|
// src/Handler/One.php
|
|
namespace App\Handler;
|
|
|
|
class One
|
|
{
|
|
public static function getDefaultIndexName(): string
|
|
{
|
|
return 'handler_one';
|
|
}
|
|
|
|
// ...
|
|
}
|
|
|
|
If you prefer to use another method name, add a ``default_index_method``
|
|
attribute to the locator service defining the name of this custom method:
|
|
|
|
.. configuration-block::
|
|
|
|
.. code-block:: yaml
|
|
|
|
# config/services.yaml
|
|
services:
|
|
# ...
|
|
|
|
App\HandlerCollection:
|
|
arguments: [!tagged_locator { tag: 'app.handler', index_by: 'key', default_index_method: 'myOwnMethodName' }]
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- config/services.xml -->
|
|
<?xml version="1.0" encoding="UTF-8" ?>
|
|
<container xmlns="http://symfony.com/schema/dic/services"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="http://symfony.com/schema/dic/services
|
|
https://symfony.com/schema/dic/services/services-1.0.xsd">
|
|
|
|
<services>
|
|
|
|
<!-- ... -->
|
|
|
|
<service id="App\HandlerCollection">
|
|
<argument type="tagged_locator" tag="app.handler" index-by="key" default-index-method="myOwnMethodName"/>
|
|
</service>
|
|
</services>
|
|
</container>
|
|
|
|
.. code-block:: php
|
|
|
|
// config/services.php
|
|
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
|
|
|
|
return function(ContainerConfigurator $configurator) {
|
|
$configurator->services()
|
|
->set(App\HandlerCollection::class)
|
|
->args([tagged_locator('app.handler', 'key', 'myOwnMethodName')])
|
|
;
|
|
};
|
|
|
|
.. note::
|
|
|
|
Since code should not be responsible for defining how the locators are
|
|
going to be used, a configuration key (``key`` in the example above) must
|
|
be set so the custom method may be called as a fallback.
|
|
|
|
Service Subscriber Trait
|
|
------------------------
|
|
|
|
The :class:`Symfony\\Contracts\\Service\\ServiceSubscriberTrait` provides an
|
|
implementation for :class:`Symfony\\Contracts\\Service\\ServiceSubscriberInterface`
|
|
that looks through all methods in your class that have no arguments and a return
|
|
type. It provides a ``ServiceLocator`` for the services of those return types.
|
|
The service id is ``__METHOD__``. This allows you to add dependencies to your
|
|
services based on type-hinted helper methods::
|
|
|
|
// src/Service/MyService.php
|
|
namespace App\Service;
|
|
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Component\Routing\RouterInterface;
|
|
use Symfony\Contracts\Service\ServiceSubscriberInterface;
|
|
use Symfony\Contracts\Service\ServiceSubscriberTrait;
|
|
|
|
class MyService implements ServiceSubscriberInterface
|
|
{
|
|
use ServiceSubscriberTrait;
|
|
|
|
public function doSomething()
|
|
{
|
|
// $this->router() ...
|
|
// $this->logger() ...
|
|
}
|
|
|
|
private function router(): RouterInterface
|
|
{
|
|
return $this->container->get(__METHOD__);
|
|
}
|
|
|
|
private function logger(): LoggerInterface
|
|
{
|
|
return $this->container->get(__METHOD__);
|
|
}
|
|
}
|
|
|
|
This allows you to create helper traits like RouterAware, LoggerAware, etc...
|
|
and compose your services with them::
|
|
|
|
// src/Service/LoggerAware.php
|
|
namespace App\Service;
|
|
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
trait LoggerAware
|
|
{
|
|
private function logger(): LoggerInterface
|
|
{
|
|
return $this->container->get(__CLASS__.'::'.__FUNCTION__);
|
|
}
|
|
}
|
|
|
|
// src/Service/RouterAware.php
|
|
namespace App\Service;
|
|
|
|
use Symfony\Component\Routing\RouterInterface;
|
|
|
|
trait RouterAware
|
|
{
|
|
private function router(): RouterInterface
|
|
{
|
|
return $this->container->get(__CLASS__.'::'.__FUNCTION__);
|
|
}
|
|
}
|
|
|
|
// src/Service/MyService.php
|
|
namespace App\Service;
|
|
|
|
use Symfony\Contracts\Service\ServiceSubscriberInterface;
|
|
use Symfony\Contracts\Service\ServiceSubscriberTrait;
|
|
|
|
class MyService implements ServiceSubscriberInterface
|
|
{
|
|
use ServiceSubscriberTrait, LoggerAware, RouterAware;
|
|
|
|
public function doSomething()
|
|
{
|
|
// $this->router() ...
|
|
// $this->logger() ...
|
|
}
|
|
}
|
|
|
|
.. caution::
|
|
|
|
When creating these helper traits, the service id cannot be ``__METHOD__``
|
|
as this will include the trait name, not the class name. Instead, use
|
|
``__CLASS__.'::'.__FUNCTION__`` as the service id.
|
|
|
|
.. _`Command pattern`: https://en.wikipedia.org/wiki/Command_pattern
|