> ## 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="폼 리퀘스트 확장">
    인증 체크나 커스텀 검증 로직을 포함하는 자체 기저 클래스를 상속한 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 뷰의 2파일을 동시 생성하기 위해 `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="/ko/advanced/package-development">
    서비스 프로바이더를 사용한 패키지 개발의 기초를 확인합니다.
  </Card>

  <Card title="Orchestra Testbench로 Laravel 패키지를 테스트하기" icon="flask-conical" href="/ko/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

- [Feed Generator](/ko/packages/laravel-bluesky/feed-generator.md)
- [튜토리얼 - Laravel Console Starter](/ko/packages/laravel-console-starter/tutorial.md)
- [디렉터리 구조](/ko/directory-structure.md)
- [Eloquent의 커스텀 캐스트](/ko/advanced/eloquent-casts.md)
- [커스텀 인증 가드 구현](/ko/advanced/custom-auth-guard.md)
