> ## 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/jp/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` に置きます。以下のクラスも、それぞれ同名のPHPファイルを `src/` に作る想定です。

```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/jp/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/jp/advanced/package-service-lifetimes.md)と併せて確認します。

また、後から `instance()` でオブジェクトを直接保存すると、通常の生成時にextenderを適用するループを通りません。テストの差し替えも含め、「登録済みだから渡した実体も自動で装飾される」とは限りません。

### すでに配られた参照は自動で置き換わらない

この例のように新しいラッパーを返す場合、コンテナの保存値が変わっても、別サービスのプロパティに保存済みの元オブジェクトは変わりません。`rebinding()` の通知は、登録済みコールバックへ新しいサービスを渡す仕組みであり、すべての依存先を探索して書き換えるものではありません。

さらに、`rebinding()` による通知コールバックの登録も、常に遅延で済むわけではありません。対象がバインド済みなら、登録時にも `make()` した結果を返します。過去に解決した通常バインディングへ `extend()` する経路でも、reboundコールバックがあれば通知のために `make()` され得ます。初期化や副作用のタイミングを一律に「次回利用時」と決めつけないでください。

## 二重装飾と状態の持ち越しを避ける

`extend()` は、同じクロージャや同じデコレーターの登録を自動で重複排除しません。このプロバイダーの `register()` を同じコンテナに2回適用すれば、接頭辞も2重になる可能性があります。

* 装飾登録はプロバイダーの決まった起動経路に集約し、リクエストやジョブごとに繰り返さない。
* 重複防止のために、全コンテナで共有する単一の静的フラグを使わない。テストや新しいアプリケーションで必要な登録が抜ける。
* singletonの装飾も同じ共有期間で保持される。現在のユーザーやリクエストなどをラッパーへ固定しない。
* `forgetExtenders()` は登録配列を消すだけで、生成済みのラッパーを元へ戻す操作ではない。

この例では出力文字列を変更しています。接頭辞・委譲する回数・例外の扱いもパッケージの公開動作になるため、単に「実装の内部変更」として扱わず、互換性を確認します。

## 同じコンテナで登録時期をテストする

次のPHPUnit例は、上記のクラスをパッケージのautoloadで読み込める前提です。プロバイダーの `register()` をContainerに直接適用し、HTTPや外部サービスを使わずに装飾の保持範囲を確認します。自動検出やアプリケーション全体の起動は別途[パッケージテスト](https://kawax.biz/jp/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/jp/advanced/package-service-resolution.md)
>
> オブジェクトを置換せずに設定するcallAfterResolving()と、適用回数の注意点を確認します。

> [card] [サービスのライフサイクル設計](https://kawax.biz/jp/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/jp/advanced/package-service-resolution.md)
- [応用トピック](https://kawax.biz/jp/advanced/index.md)
- [パッケージサービスのライフサイクル設計](https://kawax.biz/jp/advanced/package-service-lifetimes.md)
- [Laravelパッケージ開発](https://kawax.biz/jp/advanced/package-development.md)
- [パッケージの定期タスク登録と保守](https://kawax.biz/jp/advanced/package-scheduling.md)


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