Как декорировать сервисы
Дата обновления перевода 2024-07-27
Как декорировать сервисы
При переопределении существующего определения, первоначальный сервис теряется:
1 2 3 4 5 6 7 8
# config/services.yaml
services:
App\Mailer: ~
# это заменяет старое определение app.mailer новым,
# а старое определение теряется
App\Mailer:
class: App\NewMailer
В большинстве случаев это будет именно то, что вы будете хотеть сделать. Но
иногда, вы можете захотеть вместо этого декорировать старый сервис, (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
:
1 2 3 4 5 6 7 8 9 10 11
// src/DecoratingMailer.php
namespace App;
// ...
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
#[AsDecorator(decorates: Mailer::class)]
class DecoratingMailer
{
// ...
}
Опция decorates
сообщает контейнеру, что сервис App\DecoratingMailer
заменяет
сервис App\Mailer
. Если вы используете конфигурацию services.yaml по умолчанию ,
декорированный сервис автоматически внедряется, когда конструктор декориующего сервиса
имеет один аргумент с подсказкой декорированного класса сервиса.
Если вы не используете автомонтирование или декорирующий сервис имеет более одного
аргумента конструктора с подсказкой декорированного класса сервиса, ви должны внедрить
декорированный сервис ясно (ID декорированного сервиса автоматически изменяется на
'.inner'
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// src/DecoratingMailer.php
namespace App;
// ...
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
#[AsDecorator(decorates: Mailer::class)]
class DecoratingMailer
{
public function __construct(
#[AutowireDecorated]
private object $inner,
) {
}
// ...
}
Tip
Видимость декорированного сервиса App\Mailer
(который явялется
псевдонимом нового сервиса), будет такой же, как и видимость первоначального
App\Mailer
.
Note
Сгенерированный внутрений id осовывается на is сервиса декоратора (тут App\DecoratingMailer
),
а не декорированном сервисе (тут App\Mailer
). Вы можете контролировать внутренний сервис через
опцию decoration_inner_name
:
1 2 3 4 5 6
# config/services.yaml
services:
App\DecoratingMailer:
# ...
decoration_inner_name: App\DecoratingMailer.wooz
arguments: ['@App\DecoratingMailer.wooz']
Приоритет декорирования
При применении нескольких декораторов к сервису, вы не можете контролировать их порядок
с опцией decoration_priority
. Это значение является целым числом, по умолчанию 0
,
а более высокий приоритет означает, что декораторы будут применены раньше.
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 26
// ...
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
#[AsDecorator(decorates: Foo::class, priority: 5)]
class Bar
{
public function __construct(
#[AutowireDecorated]
private $inner,
) {
}
// ...
}
#[AsDecorator(decorates: Foo::class, priority: 1)]
class Baz
{
public function __construct(
#[AutowireDecorated]
private $inner,
) {
}
// ...
}
Сгенерированный код будет следующим:
1
$this->services[Foo::class] = new Baz(new Bar(new Foo()));
Стек декораторов
Альтернативой использования приоритетов декорирования является создание stack
упорядоченных сервисов, где каждый декорирует следующий:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
# config/services.yaml
services:
decorated_foo_stack:
stack:
- class: Baz
arguments: ['@.inner']
- class: Bar
arguments: ['@.inner']
- class: Foo
# используя короткий синтаксис:
decorated_foo_stack:
stack:
- Baz: ['@.inner']
- Bar: ['@.inner']
- Foo: ~
# может быть упрощенным, если включено автомонтирование:
decorated_foo_stack:
stack:
- Baz: ~
- Bar: ~
- Foo: ~
Результат будет таким же, как и в предыдущем разделе:
1
$this->services['decorated_foo_stack'] = new Baz(new Bar(new Foo()));
Как и псевдонимы, stack
может использовать только атрибуты public
и deprecated
.
Каждый фрейм в stack
может быть либо встроенным сервисом, либо ссылкой, либо дочерним
определением.
Последнее позволяет встраивание определений stack
друг в друга, вот продвинутый пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
# config/services.yaml
services:
some_decorator:
class: App\Decorator
embedded_stack:
stack:
- alias: some_decorator
- App\Decorated: ~
decorated_foo_stack:
stack:
- parent: embedded_stack
- Baz: ~
- Bar: ~
- Foo: ~
Результатом будет:
1
$this->services['decorated_foo_stack'] = new App\Decorator(new App\Decorated(new Baz(new Bar(new Foo()))));
Note
Чтобы изменить существующие стеки (то есть, из передачи компилятора), вы можете
получить доступ к каждому фрейму по его сгенерированному id со следующей структурой:
.stack_id.frame_key
.
Из примера выше, .decorated_foo_stack.1
будет ссылкой на встроенный сервис Baz
,
а .decorated_foo_stack.0
- на встроенный стек.
Чтобы получить более ясные id, вы можете дать каждому фрейму имя:
1 2 3 4 5 6 7 8
# ...
decorated_foo_stack:
stack:
first:
parent: embedded_stack
second:
Baz: ~
# ...
Id фрейма Baz
теперь будет .decorated_foo_stack.second
.
Контроль поведения, если декорированный сервис не существует
Когда вы декорируете сервис, который не существует, опция decoration_on_invalid
позволяет вам выбрать желаемое поведение.
Доступны три разных вида поведения:
exception
: Будет вызваноServiceNotFoundException
, сообщающее, что зависимость декоратора отсутствует (по умолчанию).ignore
: Контейнер удалит декоратор.null
: Контейнер оставит сервис декоратора и установит декорированный какnull
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// ...
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated;
use Symfony\Component\DependencyInjection\ContainerInterface;
#[AsDecorator(decorates: Mailer::class, onInvalid: ContainerInterface::IGNORE_ON_INVALID_REFERENCE)]
class Bar
{
public function __construct(
private #[AutowireDecorated] $inner,
) {
}
// ...
}
Caution
При использовании null
, вам может понадобиться обновить конструктор декоратора, чтобы
сделать декорированную зависимость nullable:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Service/DecoratorService.php
namespace App\Service;
use Acme\OptionalBundle\Service\OptionalService;
class DecoratorService
{
public function __construct(
private ?OptionalService $decorated,
) {
}
public function tellInterestingStuff(): string
{
if (!$this->decorated) {
return 'Just one interesting thing';
}
return $this->decorated->tellInterestingStuff().' + one more interesting thing';
}
}
Note
Иногда, вам может захотеться добавить передачу компилятора, которая создает определения
сервиса на лету. Если вы хотите декорировать такой сервис, убедитесь в том, что ваша
передача компилятора зарегистрирована с типом PassConfig::TYPE_BEFORE_OPTIMIZATION
,
чтобы передача декорации могла найти созданные сервисы.