Как декорировать сервисы

Дата обновления перевода 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, чтобы передача декорации могла найти созданные сервисы.