Як декораувати сервіси

Дата оновлення перекладу 2022-12-22

Як декораувати сервіси

При перевизначенні існуючого визначення, оригінальний сервіс втрачається:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
# config/services.yaml
services:
    App\Mailer: ~

    # це заміняє старе визначення app.mailer на нове,
    # а старе визначення втрачається
    App\Mailer:
        class: App\NewMailer

Більшість часу, це саме те, що ви хочете зробити. Однак іноді, ви можете захотіти натомість декорувати старе (тобто, застосувати патерн Декоратора). У такому випадку, старий сервіс потрібно залишшити, щоб мати можливість послатися на нього у новому. Ця конфігурація заміняє App\Mailer на нову, але залишає посилання на стару як .inner:

  • Attributes
  • YAML
  • XML
  • PHP
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
{
    // ...
}

6.1

Атрибут #[AsDecorator] було представлено в Symfony 6.1.

Опція decorates повідомляє контейнеру, що сервіс App\DecoratingMailer заміняє сервіс App\Mailer. Якщо ви використовуєте конфігурацію services.yaml за замовчуванням , декорований сервіс автоматично впроваджується, коли конструкторр сервісу, що декорує, має один аргумент з підказкою класу декорованого сервісу.

Якщо ви не використовуєте автомонтування або сервіс, що декорує, має більше одного аргументу конструктора з підказкою класу декорованого сервісу, ви повинні впровадити декорований сервіс чітко (ID декорованого сервісу автоматично змінюється на '.inner'):

  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/DecoratingMailer.php
namespace App;

// ...
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\MapDecorated;

#[AsDecorator(decorates: Mailer::class)]
class DecoratingMailer
{
    private $inner;

    public function __construct(#[MapDecorated] $inner)
    {
        $this->inner = $inner;
    }

    // ...
}

Tip

Видимість декорованого сервісу App\Mailer (що є псевдонімом нового сервісу) все ще буде така ж, як і видимість оригінального App\Mailer.

Note

Згенерований внутрішній id засновано на id сервісу декоратора (тут - App\DecoratingMailer), а не декорованого сервісу (тут - App\Mailer). Ви можете контролювати імʼя внутрішнього сервісу через опцію decoration_inner_name:

  • YAML
  • XML
  • PHP
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, та вищий пріоритет означають, що декоратори будуть застосовані раніше.

  • Attributes
  • YAML
  • XML
  • PHP
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
27
28
// ...
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\MapDecorated;

#[AsDecorator(decorates: Foo::class, priority: 5)]
class Bar
{
    private $inner;

    public function __construct(#[MapDecorated] $inner)
    {
        $this->inner = $inner;
    }
    // ...
}

#[AsDecorator(decorates: Foo::class, priority: 1)]
class Baz
{
    private $inner;

    public function __construct(#[MapDecorated] $inner)
    {
        $this->inner = $inner;
    }

    // ...
}

Згенерований код буде наступним:

1
$this->services[Foo::class] = new Baz(new Bar(new Foo()));

Стек декораторів

Альтернативою використанню пріоритетів є створення stack сервісів у порядку,ʼ де кожний декорує наступний:

  • YAML
  • XML
  • PHP
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 одне в одне, ось просунутий приклад:

  • YAML
  • XML
  • PHP
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, ви можете дати імʼя кожній рамці:

  • YAML
  • XML
  • PHP
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.
  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
use Symfony\Component\DependencyInjection\Attribute\MapDecorated;
use Symfony\Component\DependencyInjection\ContainerInterface;

#[AsDecorator(decorates: Mailer::class, onInvalid: ContainerInterface::IGNORE_ON_INVALID_REFERENCE)]
class Bar
{
    private $inner;

    public function __construct(#[MapDecorated] $inner)
    {
        $this->inner = $inner;
    }

    // ...
}

Caution

При використанні null, вам може довестися оновити конструктор декоратора для того, щоб надати декорованій залежності можливість бути null:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Service/DecoratorService.php
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

Іноді, вам може захотітися додати пропуск компілятора, який створює визначення сервісів на льоту. Якщо ви хочете декорувати такий сервіс, переконайтеся, що ваш пропуск компілятора зареєстровано з типом PassConfig::TYPE_BEFORE_OPTIMIZATION, щоб пропуск декорацій міг знайти створені сервіси.