extend() を使うと、元の生成処理を再定義せず、解決されたサービスをデコレーターで包めます。
このページは、Laravel公式ドキュメントのデフォルトブランチ 13.x と、2026年11月9日に確認したFrameworkの最新リリース v13.40.0 の公式実装・テストを参照しています。サービス解決後フックとの違いに加え、登録する時点でインスタンスが保存済みかどうかによって変わる保持範囲を扱います。
設定の追加とオブジェクトの置換を分ける
extend() のコールバックは、対象サービスとコンテナを受け取ります。同じオブジェクトを設定して返しても、新しいデコレーターを返しても構いません。return を忘れると、後続の解決処理へ null が渡ってしまいます。
ここで扱うのは
Illuminate\Container\Container::extend() です。CacheやMailなどのManagerが持つ、ドライバー名と生成処理を登録する extend() とは別のAPIです。同じ契約を実装するデコレーターを作る
元の実装を差し替えずに包む
説明用に、メッセージを整形する次の契約をsrc/MessageFormatter.php に置きます。以下のクラスも、それぞれ同名のPHPファイルを src/ に作る想定です。
PlainMessageFormatter を型指定していると、同じインターフェースを実装しただけのデコレーターでは代替できません。公開する依存先を MessageFormatter にそろえ、引数・戻り値・例外などの契約を維持します。動的なメソッド転送だけでは、PHPのインターフェース実装の代わりにはなりません。
プロバイダーで装飾を登録する
装飾を提供するsrc/PrefixServiceProvider.php は、生成方法を上書きせず、コンテナへの拡張登録だけを行います。プロバイダーの読み込みはパッケージ自動検出を参照してください。
$formatter を使います。同じキーをコールバック内で再び make() すると、解決が循環する原因になります。
利用アプリケーションの AppServiceProvider::register() などで、元の実装をバインドします。
extend() は、対象の bind() より前に登録することもできます。対象が未解決で保存済みインスタンスもない場合、登録だけで元のサービスを生成しません。公式テストでも、遅延生成とバインド前の登録が検証されています。
その後、app(MessageFormatter::class)->format('発送しました。') は [Acme] 発送しました。 を返します。元の実装を別の MessageFormatter 実装へ変更しても、その実装を包む構造です。
適用先と装飾の順序を理解する
通常の解決では、コンテナが元のサービスを生成し、登録順にextenderの戻り値へ置き換えてから、共有インスタンスとして保存し、解決後フックを呼びます。 A、Bの順で登録すると、結果はB(A(元のサービス)) です。利用側のメソッド呼び出しは外側のBから始まるので、登録順と処理開始順を混同しないでください。複数パッケージの装飾順に依存する場合は、利用アプリケーションで順序を明示し、組み合わせをテストします。
extenderは、コンテナがaliasを正規化した解決キーに対して適用します。MessageFormatter::class への登録が、PlainMessageFormatter::class を直接解決した場合にも型の一致だけで適用されるわけではありません。インターフェース経由の利用に統一します。
保存済みインスタンスへの装飾は保持方法が違う
v13.40.0 の Container::extend() は、対象のインスタンスがコンテナ内に保存されているかどうかで分岐します。
最後の行が重要です。解決済みsingletonを装飾した後で
forgetInstance() したり、同じキーを再バインドしたりしても、その場でだけ適用された装飾は次の生成時に再適用されません。最初の解決前にextender配列へ登録した場合とは異なります。
scoped() も保存済みインスタンスを使うので、スコープの途中で遅く登録した装飾を、次のスコープにも残る登録だと考えないでください。サービスのライフサイクル設計と併せて確認します。
また、後から instance() でオブジェクトを直接保存すると、通常の生成時にextenderを適用するループを通りません。テストの差し替えも含め、「登録済みだから渡した実体も自動で装飾される」とは限りません。
すでに配られた参照は自動で置き換わらない
この例のように新しいラッパーを返す場合、コンテナの保存値が変わっても、別サービスのプロパティに保存済みの元オブジェクトは変わりません。rebinding() の通知は、登録済みコールバックへ新しいサービスを渡す仕組みであり、すべての依存先を探索して書き換えるものではありません。
さらに、rebinding() による通知コールバックの登録も、常に遅延で済むわけではありません。対象がバインド済みなら、登録時にも make() した結果を返します。過去に解決した通常バインディングへ extend() する経路でも、reboundコールバックがあれば通知のために make() され得ます。初期化や副作用のタイミングを一律に「次回利用時」と決めつけないでください。
二重装飾と状態の持ち越しを避ける
extend() は、同じクロージャや同じデコレーターの登録を自動で重複排除しません。このプロバイダーの register() を同じコンテナに2回適用すれば、接頭辞も2重になる可能性があります。
- 装飾登録はプロバイダーの決まった起動経路に集約し、リクエストやジョブごとに繰り返さない。
- 重複防止のために、全コンテナで共有する単一の静的フラグを使わない。テストや新しいアプリケーションで必要な登録が抜ける。
- singletonの装飾も同じ共有期間で保持される。現在のユーザーやリクエストなどをラッパーへ固定しない。
forgetExtenders()は登録配列を消すだけで、生成済みのラッパーを元へ戻す操作ではない。
同じコンテナで登録時期をテストする
次のPHPUnit例は、上記のクラスをパッケージのautoloadで読み込める前提です。プロバイダーのregister() をContainerに直接適用し、HTTPや外部サービスを使わずに装飾の保持範囲を確認します。自動検出やアプリケーション全体の起動は別途パッケージテストで検証してください。
forgetInstance() だけを装飾の解除・再登録APIとして設計しないでください。
リリース前には、複数のextenderの順序、重複登録、instance() による差し替え、利用側がすでに保持した参照も確認します。対応Laravelバージョンを増やすときは、解決キーと型の契約に加え、extend() の分岐と公式テストを読み直します。
関連ページ
サービス解決後フック
オブジェクトを置換せずに設定するcallAfterResolving()と、適用回数の注意点を確認します。
サービスのライフサイクル設計
bind・singleton・scopedと、生成済みサービスが保持する依存関係を確認します。