> ## 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.php` 生成 `App\Handlers\OrderHandler` 类。

## 创建 stub 文件

将 stub 文件放到 `getStub()` 返回的路径。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()` 模式。先在 Service Provider 的 `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>

## 注册到 Service Provider

命令在 Service Provider 的 `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`，用户就不必手动注册 Service Provider。

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

## 应用示例

以下列举了 `GeneratorCommand` 大显身手的几个场景。

<AccordionGroup>
  <Accordion title="Form Request 扩展">
    生成继承自定义基类（包含认证检查、自定义验证逻辑）的 Request 类的命令。在 `getDefaultNamespace()` 返回 `App\Http\Requests`。
  </Accordion>

  <Accordion title="DTO 生成器">
    生成 Data Transfer Object 样板的命令。准备包含 readonly 属性、`from()` 工厂方法的 stub。
  </Accordion>

  <Accordion title="Action 类">
    生成单一职责的 Action 类的命令。在 `App\Actions` 命名空间下生成拥有 `execute()` 方法的类。
  </Accordion>

  <Accordion title="Livewire 也使用相同机制">
    `make:livewire` 命令由继承 `GeneratorCommand` 的 `MakeCommand` 类实现。为了同时生成组件类与 Blade 视图两个文件，它覆写了 `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/advanced/package-development">
    了解基于 Service Provider 的包开发基础。
  </Card>

  <Card title="使用 Orchestra Testbench 测试 Laravel 包" icon="flask-conical" href="/zh/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

- [实现自定义认证 Guard](/zh/advanced/custom-auth-guard.md)
- [Bot 教程 - Laravel Bluesky](/zh/packages/laravel-bluesky/bot-tutorial.md)
- [Eloquent 自定义类型转换](/zh/advanced/eloquent-casts.md)
- [限流的自定义](/zh/advanced/rate-limiting.md)
- [创建 AI SDK 的自定义 Provider](/zh/advanced/ai-sdk-custom-provider.md)
