> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 套件的 Artisan 指令註冊與相依性解析

> 解讀 Laravel 13 從 commands() 到 Artisan 啟動的流程，解說將指令的建立與執行分離的設計、相依性注入、延遲提供者，以及長期維護時的注意事項。

想為套件加入用於診斷、同步、安裝的 Artisan 指令時，需要將指令的註冊、實例的建立與處理的執行分開思考。若在建構子中解析僅在執行時才需要的服務，可能會連無關的 Artisan 指令啟動也受到牽連。

本頁會確認 Laravel Framework `v13.35.0` 的實作，深入探討 [Laravel 套件開發](/zh-TW/advanced/package-development)中的 `commands()` 註冊範例。檔案產生的實作請參閱 [GeneratorCommand](/zh-TW/advanced/generator-command)，參與最佳化任務請參閱[整合至 optimize](/zh-TW/advanced/package-optimization)。

## 從提供者明確註冊

套件內的指令要在服務提供者的 `boot()` 中註冊類別名稱。應用程式針對 `app/Console/Commands` 的自動偵測，與套件所提供類別的註冊是不同的入口。

```php theme={null}
<?php

namespace Acme\Courier;

use Acme\Courier\Console\StatusCommand;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        if ($this->app->runningInConsole()) {
            $this->commands([
                StatusCommand::class,
            ]);
        }
    }
}
```

此範例的前提是，提供者會透過[套件自動偵測](/zh-TW/advanced/package-discovery)或應用程式端的明確註冊而被載入。僅放置指令類別，並不會自動註冊套件的提供者。

`runningInConsole()` 判斷的是是否為主控台環境，而非是否正在執行特定指令。若也支援在 HTTP 請求中呼叫 `Artisan::call()` 的用途，就要考量因此條件而未被註冊的可能性，並在包含無條件註冊在內的選項中決定使用約定。從程式中執行的方式請參閱 [Artisan](/zh-TW/artisan)。

## commands() 不會當場執行指令

`ServiceProvider::commands()` 會將使用所接收類別名稱的閉包註冊到 `Illuminate\Console\Application::starting()`。此時並不會呼叫指令的 `handle()`。

在一般的 Console Kernel 中，之後建立 Artisan Application 時，會執行已註冊的啟動回呼。此時 `resolveCommands()` 會將各個類別傳給 `resolve()`，進行指令的註冊。

```mermaid theme={null}
flowchart TD
    A["提供者的 boot()"] --> B["以 commands() 註冊啟動回呼"]
    B --> C["Console Kernel 建立 Artisan Application"]
    C --> D["以 resolveCommands() 註冊類別"]
    D --> E["一般類別由容器實例化"]
    E --> F["執行所選的指令"]
    F --> G["解析 handle() 的相依性並呼叫"]
```

這裡的重點在於：**指令的實例建立，不一定與該指令的執行發生在同一時機**。對於不使用後文所述專用延遲註冊路徑的一般類別，`resolve()` 會呼叫容器的 `make()`，因此在註冊時就會解析建構子及其相依性。

因此，僅執行 `php artisan list` 或其他指令，套件指令的建構子也可能會執行。將 `commands()` 放在 `boot()` 中這件事本身，並不能保證實例建立會延後到執行時。

## 將執行時的相依性注入 handle()

以下以僅顯示套件設定啟用或停用狀態的指令為例。與 Laravel 13 官方 Artisan 文件相同，使用 `Signature`、`Description` 屬性定義名稱與說明。

```php theme={null}
<?php

namespace Acme\Courier\Console;

use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
use Illuminate\Contracts\Config\Repository;

#[Signature('courier:status')]
#[Description('Display whether the Courier integration is enabled')]
class StatusCommand extends Command
{
    public function handle(Repository $config): int
    {
        $enabled = (bool) $config->get('courier.enabled', false);

        $this->line($enabled ? 'Courier: enabled' : 'Courier: disabled');

        return self::SUCCESS;
    }
}
```

此範例不加入建構子，而是將設定 Repository 作為 `handle()` 的引數接收。在指令的執行路徑中，`Command::execute()` 會使用容器的 `call()`，因此會注入已指定型別的相依性。

使用外部 API 用戶端或同步服務時，若僅在執行時需要，也可以注入到相同位置。不過，若服務本身的建立或其他提供者的 `boot()` 有副作用，並不會自動防止這些副作用。

| 放置位置 | 一般指令中被求值的時機 | 適合的處理 |
| - | - | - |
| 提供者的 `boot()` | 提供者啟動時 | 註冊指令類別 |
| 指令的建構子 | 建立實例時，不一定在執行前 | 輕量的初始化 |
| `handle()` 的型別指定引數 | 指令執行時 | 解析執行所需的服務 |
| `handle()` 的主體 | 指令執行時 | 輸入驗證、業務處理、結果顯示 |

<Warning>
  請勿在建構子中連線至外部 API、更新資料庫或覆寫已發佈的檔案等。即使在剛安裝完成或連線設定尚未完成的狀態下，套件的使用者也必須能使用 `list`、`help`。
</Warning>

自行定義建構子時，也要呼叫 `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 找到指令之前的問題。

作為確保指令公開的架構，可以將註冊放在立即載入的提供者中，只將繁重服務的繫結拆分到另一個延遲提供者。具體要求請參閱[延遲服務提供者](/zh-TW/advanced/deferred-provider)。

## 更新時需要確認的事項

指令名稱、引數、選項、輸出、結束代碼，是使用者的 Shell 指令碼或 CI 所使用的公開介面。不要以變更類別內部實作的心態修改它們，而要確認相容性與遷移步驟。

| 情境 | 需要確認的事項 |
| - | - |
| 導入至全新的應用程式 | 提供者被載入，且能找到指令名稱 |
| `php artisan list` | 不會啟動套件的執行處理或外部連線 |
| `php artisan help courier:status` | 能確認說明與輸入規格 |
| 實際執行指令 | `handle()` 的相依性被解析，並得到預期的結束代碼 |
| 設定未發佈、未設定 | 清單、說明不會出錯，且能妥善處理執行時缺少的設定 |
| 更新套件或 Laravel | 既有的指令名稱、引數、選項仍可繼續使用 |

以上述的 `StatusCommand` 為例，可以在載入提供者的測試環境中，如下驗證輸出與結束代碼。測試基礎請參閱[以 Orchestra Testbench 測試 Laravel 套件](/zh-TW/advanced/package-testing)，assertion 的詳細內容請參閱[主控台測試](/zh-TW/console-tests)。

```php theme={null}
public function test_status_command_displays_enabled_state(): void
{
    config()->set('courier.enabled', true);

    $this->artisan('courier:status')
        ->expectsOutput('Courier: enabled')
        ->assertExitCode(0);
}
```

此外，對於使用繁重服務的指令，也要驗證在執行清單或說明時不會呼叫服務的業務方法。僅確認能夠註冊，無法偵測到對無關指令的影響。

## 參考的第一手資料

* [Laravel 官方文件：套件的指令註冊](https://github.com/laravel/docs/blob/13.x/packages.md#commands)
* [Laravel 官方文件：指令結構與相依性注入](https://github.com/laravel/docs/blob/13.x/artisan.md#command-structure)
* [ServiceProvider: commands()](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Support/ServiceProvider.php)
* [Console Application: starting()、bootstrap()、resolve()](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Console/Application.php)
* [Console Kernel: getArtisan() 與載入器註冊](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Foundation/Console/Kernel.php)
* [Command: 建構子與 execute()](https://github.com/laravel/framework/blob/v13.35.0/src/Illuminate/Console/Command.php)
* [官方測試：指令解析與透過 AsCommand 的別名註冊](https://github.com/laravel/framework/blob/v13.35.0/tests/Console/ConsoleApplicationTest.php)


## Related topics

- [套件的路由註冊與快取](/zh-TW/advanced/package-routes.md)
- [PHP Reflection API](/zh-TW/advanced/php-reflection.md)
- [延遲服務提供者](/zh-TW/advanced/deferred-provider.md)
- [Laravel 啟動套件的建立方式](/zh-TW/advanced/starter-kit-creation.md)
- [套件快取與 optimize 的整合](/zh-TW/advanced/package-optimization.md)


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