v13.35.0 の実装を確認し、パッケージ開発の基礎にある 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がコマンドを見つける前の問題は解決しません。
コマンドの公開を確実にする構成として、即時読み込みのプロバイダーへ登録を置き、重いサービスのバインディングだけを別の遅延プロバイダーへ分離できます。具体的な要件は遅延サービスプロバイダーを参照してください。
更新時に確認すること
コマンド名、引数、オプション、出力、終了コードは、利用者のシェルスクリプトやCIから使われる公開インターフェースです。クラス内部の変更と同じ感覚で変更せず、互換性と移行手順を確認します。
上の
StatusCommand なら、プロバイダーを読み込んだテスト環境で次のように出力と終了コードを検証できます。テスト基盤はパッケージのテスト、assertionの詳細はConsole Testsを参照してください。