v13.35.0 的實作,深入探討 Laravel 套件開發中的 commands() 註冊範例。檔案產生的實作請參閱 GeneratorCommand,參與最佳化任務請參閱整合至 optimize。
從提供者明確註冊
套件內的指令要在服務提供者的boot() 中註冊類別名稱。應用程式針對 app/Console/Commands 的自動偵測,與套件所提供類別的註冊是不同的入口。
runningInConsole() 判斷的是是否為主控台環境,而非是否正在執行特定指令。若也支援在 HTTP 請求中呼叫 Artisan::call() 的用途,就要考量因此條件而未被註冊的可能性,並在包含無條件註冊在內的選項中決定使用約定。從程式中執行的方式請參閱 Artisan。
commands() 不會當場執行指令
ServiceProvider::commands() 會將使用所接收類別名稱的閉包註冊到 Illuminate\Console\Application::starting()。此時並不會呼叫指令的 handle()。
在一般的 Console Kernel 中,之後建立 Artisan Application 時,會執行已註冊的啟動回呼。此時 resolveCommands() 會將各個類別傳給 resolve(),進行指令的註冊。
這裡的重點在於:指令的實例建立,不一定與該指令的執行發生在同一時機。對於不使用後文所述專用延遲註冊路徑的一般類別,resolve() 會呼叫容器的 make(),因此在註冊時就會解析建構子及其相依性。
因此,僅執行 php artisan list 或其他指令,套件指令的建構子也可能會執行。將 commands() 放在 boot() 中這件事本身,並不能保證實例建立會延後到執行時。
將執行時的相依性注入 handle()
以下以僅顯示套件設定啟用或停用狀態的指令為例。與 Laravel 13 官方 Artisan 文件相同,使用Signature、Description 屬性定義名稱與說明。
handle() 的引數接收。在指令的執行路徑中,Command::execute() 會使用容器的 call(),因此會注入已指定型別的相依性。
使用外部 API 用戶端或同步服務時,若僅在執行時需要,也可以注入到相同位置。不過,若服務本身的建立或其他提供者的 boot() 有副作用,並不會自動防止這些副作用。
自行定義建構子時,也要呼叫
parent::__construct()。因為 Laravel 的父類別建構子會根據屬性、signature、description 等來組成指令。
Signature 與延遲註冊是不同的機制
在所確認的Application::resolve() 中,有一個分支會讀取 Symfony AsCommand 屬性的 name,並將名稱與類別的對應儲存到 command map。此分支不會當場呼叫容器的 make()。Console Kernel 之後會呼叫 setContainerCommandLoader(),設定使用此 map 的載入器。
另一方面,上述範例使用的 Laravel Signature 屬性與這個 AsCommand 判斷無關。僅加上 Signature,並不會讓該類別進入這條延遲註冊路徑。
請將此差異視為閱讀內部實作時的注意事項,首先透過官方的指令定義與對 handle() 的相依性注入,降低建立成本與副作用。即使有延遲載入器,清單、說明等也可能需要指令資訊,因此不要以「除了執行所選指令時之外絕不會被建立」為前提來設計。
不要連指令的註冊也延遲
延遲載入服務繫結,與讓 Artisan 得知指令的存在是兩回事。若提供者實作了DeferrableProvider,在需要目標服務之前,boot() 本身可能都不會執行。
若只把指令註冊放在該 boot() 中,而在 Artisan 啟動前提供者未被載入,那麼用來告知指令存在的啟動回呼也不會被註冊。即使移至 handle() 進行相依性注入的服務會載入延遲提供者,也無法解決 Artisan 找到指令之前的問題。
作為確保指令公開的架構,可以將註冊放在立即載入的提供者中,只將繁重服務的繫結拆分到另一個延遲提供者。具體要求請參閱延遲服務提供者。
更新時需要確認的事項
指令名稱、引數、選項、輸出、結束代碼,是使用者的 Shell 指令碼或 CI 所使用的公開介面。不要以變更類別內部實作的心態修改它們,而要確認相容性與遷移步驟。
以上述的
StatusCommand 為例,可以在載入提供者的測試環境中,如下驗證輸出與結束代碼。測試基礎請參閱以 Orchestra Testbench 測試 Laravel 套件,assertion 的詳細內容請參閱主控台測試。