跳转到内容

如何装饰服务

编辑此页

当覆盖现有定义时,原始服务会丢失

1
2
3
4
5
6
7
8
# 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

大多数情况下,这正是您想要做的。但有时,您可能想要装饰旧的服务(即应用 装饰器模式)。在这种情况下,应保留旧的服务,以便在新服务中引用它。此配置用新服务替换 App\Mailer,但保留旧服务的引用为 .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,
    ) {
    }

    // ...
}

注意

被装饰的 App\Mailer 服务(它是新服务的别名)的可见性仍将与原始 App\Mailer 可见性相同。

注意

来自被装饰服务的所有自定义 服务标签 都会在新服务中被删除。只有 Symfony 定义的某些内置服务标签会被保留:container.service_locatorcontainer.service_subscriberkernel.event_subscriberkernel.event_listenerkernel.locale_awarekernel.reset

注意

生成的内部 ID 基于装饰器服务的 ID(此处为 App\DecoratingMailer),而不是被装饰服务的 ID(此处为 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

    # using the short syntax:
    decorated_foo_stack:
        stack:
            - Baz: ['@.inner']
            - Bar: ['@.inner']
            - Foo: ~

    # can be simplified when autowiring is enabled:
    decorated_foo_stack:
        stack:
            - Baz: ~
            - Bar: ~
            - Foo: ~

结果将与上一节相同

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

像别名一样,stack 只能使用 publicdeprecated 属性。

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()))));

注意

要更改现有的堆栈(即从编译器传递中),您可以使用以下结构通过其生成的 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: ~
        # ...

Baz 帧 ID 现在将是 .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(
        #[AutowireDecorated] private $inner,
    ) {
    }

    // ...
}

警告

当使用 null 时,您可能需要更新装饰器构造函数,以使被装饰的依赖项可为空

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';
    }
}

注意

有时,您可能想要添加一个编译器传递,以动态创建服务定义。如果您想要装饰这样的服务,请确保您的编译器传递注册为 PassConfig::TYPE_BEFORE_OPTIMIZATION 类型,以便装饰传递能够找到创建的服务。

本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本