.. index::
single: Service Container; Decoration
How to Decorate Services
========================
When overriding an existing definition, the original service is lost:
.. configuration-block::
.. code-block:: yaml
# config/services.yaml
services:
App\Mailer: ~
# this replaces the old App\Mailer definition with the new one, the
# old definition is lost
App\Mailer:
class: App\NewMailer
.. code-block:: xml
.. code-block:: php
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\Mailer;
use App\NewMailer;
return function(ContainerConfigurator $configurator) {
$services = $configurator->services();
$services->set(Mailer::class);
// this replaces the old App\Mailer definition with the new one, the
// old definition is lost
$services->set(Mailer::class, NewMailer::class);
};
Most of the time, that's exactly what you want to do. But sometimes,
you might want to decorate the old one instead (i.e. apply the `Decorator pattern`_).
In this case, the old service should be kept around to be able to reference
it in the new one. This configuration replaces ``App\Mailer`` with a new one,
but keeps a reference of the old one as ``.inner``:
.. configuration-block::
.. code-block:: yaml
# config/services.yaml
services:
App\Mailer: ~
App\DecoratingMailer:
# overrides the App\Mailer service
# but that service is still available as ".inner"
decorates: App\Mailer
.. code-block:: xml
.. code-block:: php
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\DecoratingMailer;
use App\Mailer;
return function(ContainerConfigurator $configurator) {
$services = $configurator->services();
$services->set(Mailer::class);
$services->set(DecoratingMailer::class)
// overrides the App\Mailer service
// but that service is still available as ".inner"
->decorate(Mailer::class);
};
The ``decorates`` option tells the container that the ``App\DecoratingMailer``
service replaces the ``App\Mailer`` service. If you're using the
:ref:`default services.yaml configuration `,
the decorated service is automatically injected when the constructor of the
decorating service has one argument type-hinted with the decorated service class.
If you are not using autowiring or the decorating service has more than one
constructor argument type-hinted with the decorated service class, you must
inject the decorated service explicitly (the ID of the decorated service is
automatically changed to ``'.inner'``):
.. configuration-block::
.. code-block:: yaml
# config/services.yaml
services:
App\Mailer: ~
App\DecoratingMailer:
decorates: App\Mailer
# pass the old service as an argument
arguments: ['@.inner']
.. code-block:: xml
.. code-block:: php
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\DecoratingMailer;
use App\Mailer;
return function(ContainerConfigurator $configurator) {
$services = $configurator->services();
$services->set(Mailer::class);
$services->set(DecoratingMailer::class)
->decorate(Mailer::class)
// pass the old service as an argument
->args([ref('.inner')]);
};
.. versionadded:: 5.1
The special ``.inner`` value was introduced in Symfony 5.1. In previous
versions you needed to use: ``decorating_service_id + '.inner'``.
.. tip::
The visibility of the decorated ``App\Mailer`` service (which is an alias
for the new service) will still be the same as the original ``App\Mailer``
visibility.
.. note::
The generated inner id is based on the id of the decorator service
(``App\DecoratingMailer`` here), not of the decorated service (``App\Mailer``
here). You can control the inner service name via the ``decoration_inner_name``
option:
.. configuration-block::
.. code-block:: yaml
# config/services.yaml
services:
App\DecoratingMailer:
# ...
decoration_inner_name: App\DecoratingMailer.wooz
arguments: ['@App\DecoratingMailer.wooz']
.. code-block:: xml
.. code-block:: php
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use App\DecoratingMailer;
use App\Mailer;
return function(ContainerConfigurator $configurator) {
$services = $configurator->services();
$services->set(Mailer::class);
$services->set(DecoratingMailer::class)
->decorate(Mailer::class, DecoratingMailer::class.'.wooz')
->args([ref(DecoratingMailer::class.'.wooz')]);
};
Decoration Priority
-------------------
When applying multiple decorators to a service, you can control their order with
the ``decoration_priority`` option. Its value is an integer that defaults to
``0`` and higher priorities mean that decorators will be applied earlier.
.. configuration-block::
.. code-block:: yaml
# config/services.yaml
Foo: ~
Bar:
decorates: Foo
decoration_priority: 5
arguments: ['@.inner']
Baz:
decorates: Foo
decoration_priority: 1
arguments: ['@.inner']
.. code-block:: xml
.. code-block:: php
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
return function(ContainerConfigurator $configurator) {
$services = $configurator->services();
$services->set(Foo::class);
$services->set(Bar::class)
->decorate(Foo::class, null, 5)
->args([ref('.inner')]);
$services->set(Baz::class)
->decorate(Foo::class, null, 1)
->args([ref('.inner')]);
};
The generated code will be the following::
$this->services[Foo::class] = new Baz(new Bar(new Foo()));
Control the Behavior When the Decorated Service Does Not Exist
--------------------------------------------------------------
When you decorate a service that doesn't exist, the ``decoration_on_invalid``
option allows you to choose the behavior to adopt.
Three different behaviors are available:
* ``exception``: A ``ServiceNotFoundException`` will be thrown telling that decorator's dependency is missing. (default)
* ``ignore``: The container will remove the decorator.
* ``null``: The container will keep the decorator service and will set the decorated one to ``null``.
.. configuration-block::
.. code-block:: yaml
# config/services.yaml
Foo: ~
Bar:
decorates: Foo
decoration_on_invalid: ignore
arguments: ['@.inner']
.. code-block:: xml
.. code-block:: php
// config/services.php
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
use Symfony\Component\DependencyInjection\ContainerInterface;
return function(ContainerConfigurator $configurator) {
$services = $configurator->services();
$services->set(Foo::class);
$services->set(Bar::class)
->decorate(Foo::class, null, 0, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)
->args([ref('.inner')])
;
};
.. caution::
When using ``null``, you may have to update the decorator constructor in
order to make decorated dependency nullable::
namespace App\Service;
use Acme\OptionalBundle\Service\OptionalService;
class DecoratorService
{
private $decorated;
public function __construct(?OptionalService $decorated)
{
$this->decorated = $decorated;
}
public function tellInterestingStuff(): string
{
if (!$this->decorated) {
return 'Just one interesting thing';
}
return $this->decorated->tellInterestingStuff().' + one more interesting thing';
}
}
.. note::
Sometimes, you may want to add a compiler pass that creates service
definitions on the fly. If you want to decorate such a service,
be sure that your compiler pass is registered with ``PassConfig::TYPE_BEFORE_OPTIMIZATION``
type so that the decoration pass will be able to find the created services.
.. _`Decorator pattern`: https://en.wikipedia.org/wiki/Decorator_pattern