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。以下各個類別也假設分別在 src/ 中建立同名的 PHP 檔案。
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(),前綴也可能被加上兩次。
- 將裝飾註冊集中在提供者固定的啟動路徑,不要在每個請求或工作中重複。
- 不要為了防止重複而使用所有容器共用的單一靜態旗標。測試或新的應用程式會缺少必要的註冊。
- singleton 的裝飾也會在相同的共用期間內保留。不要將目前的使用者或請求等固定到包裝器中。
forgetExtenders()只會清除註冊陣列,並不是將已建立的包裝器還原的操作。
以同一個容器測試註冊時機
以下 PHPUnit 範例假設上述類別可以透過套件的 autoload 載入。它將提供者的register() 直接套用到 Container,在不使用 HTTP 或外部服務的情況下確認裝飾的保留範圍。自動偵測與整個應用程式的啟動,請另外透過以 Orchestra Testbench 測試 Laravel 套件驗證。
forgetInstance() 單獨設計為解除或重新註冊裝飾的 API。
發布前,也請確認多個 extender 的順序、重複註冊、透過 instance() 的替換,以及使用端已持有的參照。增加支援的 Laravel 版本時,除了解析鍵與型別契約之外,也要重新閱讀 extend() 的分支與官方測試。
相關頁面
套件的服務解析後掛鉤
確認不替換物件而進行設定的 callAfterResolving(),以及套用次數的注意事項。
套件服務的生命週期設計
確認 bind・singleton・scoped,以及已建立服務所持有的相依關係。