> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 包的服务装饰与 extend

> 解读 Laravel 13 的 Container::extend()，讲解保持契约的装饰器、解析前后的注册、singleton 的保留范围、双重装饰以及升级时的测试。

有时你希望在保留使用方应用所选服务实现的同时，从包中追加处理。使用容器的 `extend()`，无需重新定义原有的创建逻辑，就能用装饰器包装已解析的服务。

本页参考 Laravel 官方文档的默认分支 `13.x`，以及 2026 年 11 月 9 日确认的 Framework 最新版本 `v13.40.0` 的官方实现与测试。除了与[包的服务解析后钩子](https://kawax.biz/zh-CN/advanced/package-service-resolution.md)的区别外，还会说明随 **注册时实例是否已被保存** 而变化的保留范围。

## 区分添加配置与替换对象

| API | 用途 | 回调的返回值 |
| - | - | - |
| `bind()`・`singleton()` | 注册服务的创建方式和是否共享 | 要创建的服务 |
| `afterResolving()` | 对已解析的服务应用配置等 | 不用于替换服务 |
| `extend()` | 保留创建逻辑，对服务进行装饰或修改 | 必须返回修改后的服务 |

`extend()` 的回调会接收目标服务和容器。既可以配置同一个对象后返回，也可以返回新的装饰器。如果忘记 `return`，后续的解析处理会收到 `null`。

> [!NOTE]
>
> 这里讨论的是 `Illuminate\Container\Container::extend()`。它与 Cache、Mail 等 Manager 所具有的、用于注册驱动名称和创建逻辑的 `extend()` 是不同的 API。

## 创建实现相同契约的装饰器

### 不替换原有实现而是进行包装

为便于说明，将以下用于格式化消息的契约放在 `src/MessageFormatter.php` 中。以下各个类也假定分别在 `src/` 中创建同名的 PHP 文件。

```php theme={null}
<?php

namespace Acme\Messages;

interface MessageFormatter
{
    public function format(string $message): string;
}
```

装饰前的实现会原样返回输入。

```php theme={null}
<?php

namespace Acme\Messages;

final class PlainMessageFormatter implements MessageFormatter
{
    public function format(string $message): string
    {
        return $message;
    }
}
```

装饰器实现相同的接口，并将处理委托给传入的服务。本例中，会在委托对象的返回值前加上包特有的前缀。

```php theme={null}
<?php

namespace Acme\Messages;

final class PrefixedMessageFormatter implements MessageFormatter
{
    public function __construct(
        private readonly MessageFormatter $inner,
    ) {}

    public function format(string $message): string
    {
        return '[Acme] '.$this->inner->format($message);
    }
}
```

如果使用方将具体类 `PlainMessageFormatter` 作为类型声明，那么仅实现相同接口的装饰器无法替代它。请将公开的依赖统一为 `MessageFormatter`，并保持参数、返回值、异常等契约。仅靠动态的方法转发，无法替代 PHP 的接口实现。

### 在提供者中注册装饰

提供装饰的 `src/PrefixServiceProvider.php` 不会覆盖创建方式，只向容器注册扩展。关于提供者的加载，请参阅[包自动发现的内部结构](https://kawax.biz/zh-CN/advanced/package-discovery.md)。

```php theme={null}
<?php

namespace Acme\Messages;

use Illuminate\Support\ServiceProvider;

final class PrefixServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->extend(
            MessageFormatter::class,
            static fn (MessageFormatter $formatter): MessageFormatter =>
                new PrefixedMessageFormatter($formatter),
        );
    }
}
```

回调的第 2 个参数也会传入容器，但本例不需要，因此没有声明。目标服务使用参数 `$formatter`。如果在回调内再次对同一个键调用 `make()`，会导致解析循环。

在使用方应用的 `AppServiceProvider::register()` 等位置绑定原有实现。

```php theme={null}
use Acme\Messages\MessageFormatter;
use Acme\Messages\PlainMessageFormatter;

$this->app->bind(MessageFormatter::class, PlainMessageFormatter::class);
```

`extend()` 也可以在目标的 `bind()` 之前注册。如果目标尚未解析且没有已保存的实例，仅注册不会创建原有服务。官方测试中也验证了延迟创建和绑定前的注册。

之后，`app(MessageFormatter::class)->format('已发货。')` 会返回 `[Acme] 已发货。`。即使把原有实现换成其他 `MessageFormatter` 实现，装饰器也会包装那个实现。

> [!WARNING]
>
> 原则上请在首次解析之前注册。其他提供者可能会先使用该服务，因此仅仅移到 `boot()` 并不能防止注册前的解析。另外，如果轻易地把只负责注册扩展的提供者设为延迟提供者，注册本身可能会失去触发条件。

## 理解应用目标与装饰顺序

在通常的解析中，容器会创建原有服务，按注册顺序替换为各 extender 的返回值，然后作为共享实例保存，并调用解析后钩子。

```mermaid theme={null}
flowchart LR
    A["原有创建逻辑"] --> B["服务"]
    B --> C["extender A<br>用 Wrapper A 包装"]
    C --> D["extender B<br>用 Wrapper B 包装"]
    D --> E["如果共享则保存"]
    E --> F["解析后钩子・使用方"]
```

按 A、B 的顺序注册时，结果为 `B(A(原有服务))`。使用方的方法调用从外侧的 B 开始，因此不要混淆注册顺序与处理开始顺序。如果依赖多个包的装饰顺序，请在使用方应用中明确顺序，并测试组合。

extender 应用于容器规范化 alias 之后的 **解析键**。对 `MessageFormatter::class` 的注册，并不会仅因类型一致就应用于直接解析 `PlainMessageFormatter::class` 的情况。请统一通过接口使用。

## 对已保存实例的装饰，保留方式不同

`v13.40.0` 的 `Container::extend()` 会根据目标实例是否已保存在容器中而分支。

| 注册时的状态 | `extend()` 的行为 | 对将来新创建的保留 |
| - | - | - |
| 尚未解析，也没有已保存的实例 | 添加到 extender 数组 | 在之后的通常创建时应用 |
| 过去解析过普通 `bind()`，但没有已保存的实例 | 添加 extender 并尝试发送 rebound 通知 | 在之后的通常创建时应用 |
| 已保存 singleton 或 `instance()` 的对象 | 当场装饰，将保存值替换为返回值并发送 rebound 通知 | **本次的 extender 不会添加到数组** |

最后一行很重要。装饰已解析的 singleton 之后，即使调用 `forgetInstance()` 或重新绑定同一个键，**仅当场应用的装饰** 也不会在下次创建时重新应用。这与在首次解析前注册到 extender 数组的情况不同。

`scoped()` 也会使用已保存的实例，因此不要认为在作用域中途延迟注册的装饰也会保留到下一个作用域。请结合[包服务的生命周期设计](https://kawax.biz/zh-CN/advanced/package-service-lifetimes.md)一起确认。

另外，如果之后用 `instance()` 直接保存对象，它不会经过通常创建时应用 extender 的循环。包括测试中的替换在内，并不一定是"已经注册了，所以传入的实体也会被自动装饰"。

### 已经分发出去的引用不会被自动替换

像本例这样返回新的包装器时，即使容器的保存值发生变化，已经保存在其他服务属性中的原对象也不会改变。`rebinding()` 的通知是将新服务传给已注册回调的机制，并不会查找并改写所有依赖方。

此外，通过 `rebinding()` 注册通知回调也并不总是延迟完成。如果目标已绑定，注册时也会返回 `make()` 的结果。在对过去解析过的普通绑定执行 `extend()` 的路径中，如果存在 rebound 回调，也可能为了通知而执行 `make()`。不要一概认定初始化和副作用的时机是"下次使用时"。

## 避免双重装饰和状态延续

`extend()` 不会自动对同一个闭包或同一个装饰器的注册去重。如果对同一个容器应用两次该提供者的 `register()`，前缀也可能被加上两次。

* 将装饰注册集中在提供者固定的启动路径中，不要在每个请求或任务中重复。
* 不要为了防止重复而使用所有容器共享的单个静态标志。测试或新的应用中会缺少必要的注册。
* singleton 的装饰也会在相同的共享期间内保留。不要把当前用户或请求等固定到包装器中。
* `forgetExtenders()` 只会清除注册数组，并不是把已创建的包装器恢复原状的操作。

本例修改了输出字符串。前缀、委托次数和异常处理也会成为包的公开行为，因此不要简单地视为"实现的内部变更"，请确认兼容性。

## 用同一个容器测试注册时机

以下 PHPUnit 示例假定上述类可以通过包的 autoload 加载。它将提供者的 `register()` 直接应用于 Container，在不使用 HTTP 或外部服务的情况下确认装饰的保留范围。自动发现和整个应用的启动请另外通过[使用 Orchestra Testbench 测试 Laravel 包](https://kawax.biz/zh-CN/advanced/package-testing.md)进行验证。

```php theme={null}
<?php

namespace Acme\Messages\Tests;

use Acme\Messages\MessageFormatter;
use Acme\Messages\PlainMessageFormatter;
use Acme\Messages\PrefixedMessageFormatter;
use Acme\Messages\PrefixServiceProvider;
use Illuminate\Container\Container;
use PHPUnit\Framework\TestCase;

final class ServiceDecorationTest extends TestCase
{
    public function test_extension_is_lazy_and_can_precede_binding(): void
    {
        $container = new Container;
        $builds = 0;

        (new PrefixServiceProvider($container))->register();
        $container->bind(MessageFormatter::class, function () use (&$builds) {
            $builds++;

            return new PlainMessageFormatter;
        });

        $this->assertSame(0, $builds);

        $first = $container->make(MessageFormatter::class);
        $second = $container->make(MessageFormatter::class);

        $this->assertSame('[Acme] ready', $first->format('ready'));
        $this->assertSame('[Acme] ready', $second->format('ready'));
        $this->assertNotSame($first, $second);
        $this->assertSame(2, $builds);
    }

    public function test_early_extension_survives_singleton_recreation(): void
    {
        $container = new Container;
        $container->singleton(MessageFormatter::class, PlainMessageFormatter::class);
        (new PrefixServiceProvider($container))->register();

        $first = $container->make(MessageFormatter::class);

        $this->assertInstanceOf(PrefixedMessageFormatter::class, $first);
        $this->assertSame($first, $container->make(MessageFormatter::class));

        $container->forgetInstance(MessageFormatter::class);
        $second = $container->make(MessageFormatter::class);

        $this->assertNotSame($first, $second);
        $this->assertSame('[Acme] ready', $second->format('ready'));
    }

    public function test_late_singleton_extension_is_not_retained_as_a_callback(): void
    {
        $container = new Container;
        $container->singleton(MessageFormatter::class, PlainMessageFormatter::class);
        $original = $container->make(MessageFormatter::class);

        (new PrefixServiceProvider($container))->register();
        $decorated = $container->make(MessageFormatter::class);

        $this->assertInstanceOf(PrefixedMessageFormatter::class, $decorated);
        $this->assertNotSame($original, $decorated);
        $this->assertSame('ready', $original->format('ready'));
        $this->assertSame('[Acme] ready', $decorated->format('ready'));

        $container->forgetInstance(MessageFormatter::class);
        $recreated = $container->make(MessageFormatter::class);

        $this->assertInstanceOf(PlainMessageFormatter::class, $recreated);
        $this->assertSame('ready', $recreated->format('ready'));
    }
}
```

最后一个测试并不是推荐延迟注册，而是明确所确认版本的边界。不要把 `forgetInstance()` 单独设计为解除或重新注册装饰的 API。

发布前，还要确认多个 extender 的顺序、重复注册、通过 `instance()` 的替换，以及使用方已经持有的引用。增加支持的 Laravel 版本时，除了解析键和类型契约外，还要重新阅读 `extend()` 的分支和官方测试。

## 相关页面

> [card] [包的服务解析后钩子](https://kawax.biz/zh-CN/advanced/package-service-resolution.md)
>
> 了解不替换对象而进行配置的 callAfterResolving()，以及应用次数的注意事项。

> [card] [包服务的生命周期设计](https://kawax.biz/zh-CN/advanced/package-service-lifetimes.md)
>
> 了解 bind・singleton・scoped，以及已创建服务所持有的依赖关系。

## 参考的一手资料

* [Laravel 官方文档：Extending bindings・Rebinding](https://github.com/laravel/docs/blob/13.x/container.md#extending-bindings)
* [Laravel 官方文档：Service Providers](https://github.com/laravel/docs/blob/13.x/providers.md)
* [Laravel 官方文档：包开发与自动发现](https://github.com/laravel/docs/blob/13.x/packages.md)
* [Container：extend()・resolve()・rebound()・instance()・forgetExtenders()](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Container/Container.php)
* [官方测试：延迟创建・绑定前的注册・alias・多个 extender](https://github.com/laravel/framework/blob/v13.40.0/tests/Container/ContainerExtendTest.php)
* [ServiceProvider：构造函数与 register()](https://github.com/laravel/framework/blob/v13.40.0/src/Illuminate/Support/ServiceProvider.php)


## Related topics

- [包的服务解析后钩子](https://kawax.biz/zh-CN/advanced/package-service-resolution.md)
- [进阶主题](https://kawax.biz/zh-CN/advanced/index.md)
- [包的自定义缓存存储与契约](https://kawax.biz/zh-CN/advanced/package-cache-drivers.md)
- [包的邮件传输注册与测试](https://kawax.biz/zh-CN/advanced/package-mail-transports.md)
- [包的容器标签与插件聚合](https://kawax.biz/zh-CN/advanced/package-container-tags.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.