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

# GeneratorCommand — 自訂 make: 指令的實作

> 說明如何繼承 Illuminate\Console\GeneratorCommand，建立套件專屬的 make: 指令。從 stub 檔案的建立到命名空間的控制，實務地介紹。

## 什麼是 GeneratorCommand

`Illuminate\Console\GeneratorCommand` 是 Laravel 所有程式碼產生指令的基底抽象類別。`make:model`、`make:controller`、`make:request` 等皆繼承此類別。

```mermaid theme={null}
flowchart TD
    A["GeneratorCommand<br>(抽象類別)"] --> B["make:model<br>ModelMakeCommand"]
    A --> C["make:request<br>RequestMakeCommand"]
    A --> D["make:controller<br>ControllerMakeCommand"]
    A --> E["make:handler<br>您的指令"]
```

若套件提供自訂的 `make:xxx` 指令，使用者無需手動建立類別，只要執行 `php artisan make:handler OrderHandler` 之類的指令，即可產生具備正確命名空間的檔案。

<Info>
  `GeneratorCommand` 在官方文件中幾乎沒有記載，須直接閱讀框架原始碼才能了解。這是典型的進階主題。
</Info>

## 最小實作

繼承 `GeneratorCommand` 的類別必要實作只有 `getStub()` 方法。其他屬性為選用，但實務上通常會齊備下列項目。

| 成員                      | 種類     | 角色                          |
| ----------------------- | ------ | --------------------------- |
| `$name`                 | 屬性     | 指令名稱（例：`make:handler`）      |
| `$description`          | 屬性     | 指令的說明文字                     |
| `$type`                 | 屬性     | 生成物種類名稱，用於成功訊息（例：`Handler`） |
| `getStub()`             | 方法（必要） | 回傳 stub 檔案的路徑               |
| `getDefaultNamespace()` | 方法     | 控制產生目的地的預設命名空間              |

以下為實作 `make:handler` 指令的範例。

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

namespace Vendor\Package\Console\Commands;

use Illuminate\Console\GeneratorCommand;

class HandlerMakeCommand extends GeneratorCommand
{
    protected $name = 'make:handler';

    protected $description = 'Create a new handler class';

    protected $type = 'Handler';

    protected function getStub(): string
    {
        return __DIR__.'/stubs/handler.stub';
    }

    protected function getDefaultNamespace($rootNamespace): string
    {
        return $rootNamespace.'\Handlers';
    }
}
```

設定 `getDefaultNamespace()` 後，執行 `php artisan make:handler OrderHandler` 時，會將 `App\Handlers\OrderHandler` 類別產生至 `app/Handlers/OrderHandler.php`。

## 建立 stub 檔案

於 `getStub()` 回傳的路徑放置 stub 檔案。stub 是作為範本的 PHP 檔案，其中的預留位置會被替換為命名空間與類別名稱。

<Tree>
  <Tree.Folder name="src" defaultOpen>
    <Tree.Folder name="Console" defaultOpen>
      <Tree.Folder name="Commands" defaultOpen>
        <Tree.File name="HandlerMakeCommand.php" />

        <Tree.Folder name="stubs" defaultOpen>
          <Tree.File name="handler.stub" />
        </Tree.Folder>
      </Tree.Folder>
    </Tree.Folder>
  </Tree.Folder>
</Tree>

stub 檔案範例：

```php handler.stub theme={null}
<?php

namespace {{ namespace }};

class {{ class }}
{
    public function handle(): void
    {
        //
    }
}
```

`GeneratorCommand` 會自動替換 stub 中以下的預留位置。

| 預留位置                  | 替換後的值      | 替代表示                 |
| --------------------- | ---------- | -------------------- |
| `{{ namespace }}`     | 生成類別的命名空間  | `DummyNamespace`     |
| `{{ class }}`         | 生成類別名稱     | `DummyClass`         |
| `{{ rootNamespace }}` | 應用程式的根命名空間 | `DummyRootNamespace` |

`{{ namespace }}` 與 `DummyNamespace` 兩種寫法均可獲得相同結果。Laravel 內建的 stub 兩種形式皆有，但新建時建議使用 `{{ namespace }}` 形式。

## 讓 stub 可被自訂

若要提供允許使用者覆寫 stub 的機制，可以採用 `resolveStubPath()` 模式。首先於服務提供者的 `boot()` 中公開 stub。

```php theme={null}
public function boot(): void
{
    if ($this->app->runningInConsole()) {
        $this->publishes([
            __DIR__.'/../Console/Commands/stubs' => base_path('stubs'),
        ], 'stubs');
    }
}
```

接著於 `getStub()` 優先使用使用者已自訂的 stub。

```php theme={null}
protected function getStub(): string
{
    return $this->resolveStubPath('/stubs/handler.stub');
}

protected function resolveStubPath(string $stub): string
{
    return file_exists($customPath = $this->laravel->basePath(trim($stub, '/')))
        ? $customPath
        : __DIR__.$stub;
}
```

執行 `php artisan vendor:publish --tag=stubs` 的使用者可透過編輯專案根目錄的 `stubs/handler.stub` 來變更範本。

<Tip>
  `resolveStubPath()` 也是 Laravel 內建 `RequestMakeCommand` 所使用的模式。若要發布套件，建議採用此模式。
</Tip>

## 於服務提供者註冊

指令於服務提供者的 `boot()` 方法中註冊。透過 `runningInConsole()` 檢查可以避免 Web 請求時多餘的載入。

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

namespace Vendor\Package;

use Illuminate\Support\ServiceProvider;
use Vendor\Package\Console\Commands\HandlerMakeCommand;

class PackageServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        if ($this->app->runningInConsole()) {
            $this->commands([
                HandlerMakeCommand::class,
            ]);

            $this->publishes([
                __DIR__.'/../Console/Commands/stubs' => base_path('stubs'),
            ], 'stubs');
        }
    }
}
```

若在套件的 `composer.json` 設定 `extra.laravel`，使用者就無需手動註冊服務提供者。

```json theme={null}
"extra": {
    "laravel": {
        "providers": [
            "Vendor\\Package\\PackageServiceProvider"
        ]
    }
}
```

## 活用範例

以下列舉 `GeneratorCommand` 大顯身手的幾種情境。

<AccordionGroup>
  <Accordion title="擴充 Form Request">
    產生繼承包含驗證檢查或自訂驗證邏輯之獨自基底類別的 Request 指令。以 `getDefaultNamespace()` 回傳 `App\Http\Requests`。
  </Accordion>

  <Accordion title="DTO Generator">
    產生 Data Transfer Object 樣板的指令。準備包含 readonly 屬性與 `from()` factory 方法的 stub。
  </Accordion>

  <Accordion title="Action 類別">
    產生單一職責 Action 類別的指令。在 `App\Actions` 命名空間下產生具有 `execute()` 方法的類別。
  </Accordion>

  <Accordion title="Livewire 使用相同的機制">
    `make:livewire` 指令由繼承 `GeneratorCommand` 的 `MakeCommand` 類別實作。為了同時產生元件類別與 Blade view 兩個檔案，覆寫了 `handle()`。
  </Accordion>
</AccordionGroup>

## 測試

使用 Orchestra Testbench，測試指令是否正確產生檔案。

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

namespace Tests\Feature\Console;

use Illuminate\Support\Facades\File;
use Orchestra\Testbench\TestCase;
use Vendor\Package\PackageServiceProvider;

class HandlerMakeCommandTest extends TestCase
{
    protected function getPackageProviders($app): array
    {
        return [PackageServiceProvider::class];
    }

    protected function tearDown(): void
    {
        File::deleteDirectory(app_path('Handlers'));

        parent::tearDown();
    }

    public function test_make_handler_creates_file(): void
    {
        $path = app_path('Handlers/OrderHandler.php');

        $this->artisan('make:handler', ['name' => 'OrderHandler'])
            ->assertSuccessful();

        $this->assertFileExists($path);
        $this->assertStringContainsString('namespace App\Handlers;', File::get($path));
        $this->assertStringContainsString('class OrderHandler', File::get($path));
    }

    public function test_make_handler_does_not_overwrite_existing_file(): void
    {
        $path = app_path('Handlers/OrderHandler.php');
        File::ensureDirectoryExists(dirname($path));
        File::put($path, '<?php // existing');

        $this->artisan('make:handler', ['name' => 'OrderHandler'])
            ->assertFailed();
    }
}
```

<Warning>
  Generator 指令會對實際的檔案系統寫入。務必於 `tearDown()` 進行清理。即使測試中途失敗也能確實執行。
</Warning>

## 相關頁面

<Columns cols={2}>
  <Card title="Laravel 套件開發" icon="package" href="/zh-TW/advanced/package-development">
    確認使用服務提供者進行套件開發的基礎。
  </Card>

  <Card title="以 Orchestra Testbench 測試 Laravel 套件" icon="flask-conical" href="/zh-TW/advanced/package-testing">
    確認套件測試基礎的建立方式。
  </Card>
</Columns>

<Info>
  Source: [Illuminate\Console\GeneratorCommand](https://github.com/laravel/framework/blob/master/src/Illuminate/Console/GeneratorCommand.php), [Illuminate\Foundation\Console\RequestMakeCommand](https://github.com/laravel/framework/blob/master/src/Illuminate/Foundation/Console/RequestMakeCommand.php)
</Info>


## Related topics

- [Eloquent 的自訂 Cast](/zh-TW/advanced/eloquent-casts.md)
- [Laravel 啟動套件的建立方式](/zh-TW/advanced/starter-kit-creation.md)
- [自訂驗證 Guard 的實作](/zh-TW/advanced/custom-auth-guard.md)
- [教學 - Laravel Console Starter](/zh-TW/packages/laravel-console-starter/tutorial.md)
- [Feed Generator](/zh-TW/packages/laravel-bluesky/feed-generator.md)
