> ## 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의 Artisan 콘솔을 사용해 커스텀 CLI 명령어를 작성하는 방법을 설명합니다.

## Artisan이란

Artisan은 Laravel에 포함되어 있는 커맨드라인 인터페이스(CLI) 도구입니다.
프로젝트 루트의 `artisan` 스크립트로 존재하며, 개발과 운영을 효율화하는 다수의 명령어를 제공합니다.

사용 가능한 명령어 목록을 확인하려면 `list` 명령어를 사용합니다.

```shell theme={null}
php artisan list
```

명령어의 사용법을 확인하려면 `help`를 앞에 붙입니다.

```shell theme={null}
php artisan help migrate
```

<Info>
  Laravel Sail을 사용하는 경우, `php artisan` 대신에 `sail artisan`을 사용하십시오.
  명령어는 Docker 컨테이너 내에서 실행됩니다.

  ```shell theme={null}
  ./vendor/bin/sail artisan list
  ```
</Info>

## Tinker

[Laravel Tinker](https://github.com/laravel/tinker)는 Eloquent 모델이나 Job, 이벤트 등 애플리케이션 전체를 커맨드라인 상에서 인터랙티브하게 조작할 수 있는 REPL 환경입니다.

```shell theme={null}
php artisan tinker
```

기동 후에는 PHP 코드를 그대로 실행할 수 있습니다.

```php theme={null}
// 사용자를 조회해 확인
>>> App\Models\User::find(1)
// 팩토리로 레코드를 생성
>>> App\Models\User::factory()->create()
// 서비스 클래스를 직접 호출
>>> app(App\Services\OrderService::class)->process(1)
```

<Tip>
  Tinker는 데이터를 실제로 변경합니다. 운영 환경에서의 실행은 충분히 주의하십시오.
  로컬이나 스테이징 환경에서의 동작 확인, 디버깅 용도에 적합합니다.
</Tip>

## 커스텀 명령어 작성

### 명령어 생성

`make:command`로 명령어 클래스의 골격을 생성합니다.

```shell theme={null}
php artisan make:command ImportProducts
```

`app/Console/Commands/ImportProducts.php`가 생성됩니다.

### 명령어의 기본 구조

생성된 명령어 클래스에는 `$signature`, `$description`, `handle()`이라는 3가지 주요 요소가 있습니다.

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

namespace App\Console\Commands;

use Illuminate\Console\Command;

class ImportProducts extends Command
{
    /**
     * 명령어 이름과 인수·옵션 정의
     */
    protected $signature = 'import:products';

    /**
     * 명령어 설명(php artisan list 에 표시됨)
     */
    protected $description = 'CSV로부터 상품 데이터를 임포트합니다';

    /**
     * 명령어의 실행 처리
     */
    public function handle(): void
    {
        $this->info('임포트를 시작합니다...');
        // 처리 기술
        $this->info('완료했습니다.');
    }
}
```

<Info>
  Laravel 13에서는 PHP 어트리뷰트를 사용한 작성법도 이용할 수 있습니다.

  ```php theme={null}
  use Illuminate\Console\Attributes\Description;
  use Illuminate\Console\Attributes\Signature;

  #[Signature('import:products')]
  #[Description('CSV로부터 상품 데이터를 임포트합니다')]
  class ImportProducts extends Command
  {
      public function handle(): void
      {
          // ...
      }
  }
  ```
</Info>

### 인수와 옵션의 정의

`$signature`에 명령어 이름, 인수, 옵션을 함께 정의합니다.

```php theme={null}
protected $signature = 'import:products
                        {file : 임포트할 CSV 파일의 경로}
                        {--limit= : 임포트할 최대 건수}
                        {--dry-run : 실제로는 DB에 저장하지 않음(확인용)}';
```

| 표기법                   | 종류         | 설명                        |
| --------------------- | ---------- | ------------------------- |
| `{file}`              | 필수 인수      | 생략하면 오류가 발생               |
| `{file?}`             | 생략 가능한 인수  | 생략한 경우는 `null`            |
| `{file=products.csv}` | 기본값이 있는 인수 | 생략한 경우 기본값 사용             |
| `{--queue}`           | 플래그 옵션     | 지정하면 `true`, 생략하면 `false` |
| `{--limit=}`          | 값이 있는 옵션   | `--limit=100`처럼 값을 전달     |
| `{--limit=50}`        | 기본값이 있는 옵션 | 생략 시 `50`                 |
| `{--Q\|queue}`        | 단축키가 있는    | `-Q`로도 지정 가능              |

### 인수와 옵션의 취득

`handle()` 메서드 내에서 `argument()`와 `option()`을 사용해 값을 취득합니다.

```php theme={null}
public function handle(): void
{
    $file    = $this->argument('file');       // 인수 취득
    $limit   = $this->option('limit');        // 옵션 취득
    $dryRun  = $this->option('dry-run');      // 플래그 옵션(bool)

    $this->info("파일: {$file}");

    if ($dryRun) {
        $this->warn('드라이 런 모드: DB에의 저장은 스킵합니다');
    }
}
```

### `input()` — 타입 지정 접근자

`input()` 메서드를 사용하면 HTTP 요청과 동일한 타입 지정 접근자로 명령어의 인수나 옵션을 취득할 수 있습니다.

```php theme={null}
use App\Enums\ReportType;

public function handle(): void
{
    // 인수·옵션을 CommandInput 인스턴스로 취득
    $from = $this->input()->date('from');
    $type = $this->input()->enum('type', ReportType::class);
    $limit = $this->input()->integer('limit');
}
```

특정 이름만 취득하고 싶은 경우 이름을 직접 전달합니다(인수와 옵션 양쪽에서 검색됩니다).

```php theme={null}
$queue = $this->input('queue', 'default');
```

<Tip>
  `argument()`나 `option()`이 문자열을 반환하는 데 반해, `input()`의 타입 지정 접근자는 적절한 타입으로 변환합니다. 날짜, 숫자, Enum 등 타입 안전하게 다루고 싶은 경우에 편리합니다.
</Tip>

## 사용자와의 인터랙션

### 메시지 출력

콘솔에 색상이 있는 메시지를 출력하는 메서드가 준비되어 있습니다.

```php theme={null}
$this->info('처리가 정상적으로 완료되었습니다');    // 초록
$this->warn('이 조작은 되돌릴 수 없습니다');    // 노랑
$this->error('오류가 발생했습니다');       // 빨강
$this->line('일반 텍스트');             // 색 없음
$this->comment('보충 정보');               // 옅은 회색
```

### 사용자에게 질문

대화적으로 사용자로부터 입력을 받을 수 있습니다.

```php theme={null}
// 텍스트 입력
$name = $this->ask('담당자명을 입력하십시오');

// 기본값 포함
$env = $this->ask('실행 환경을 선택', 'production');

// 비밀번호 등 비밀 정보(입력이 화면에 표시되지 않음)
$password = $this->secret('API 키를 입력하십시오');

// Yes/No 확인
if (! $this->confirm('운영 DB에 임포트하시겠습니까?')) {
    $this->info('취소했습니다.');
    return;
}

// 선택지에서 선택
$format = $this->choice('출력 형식을 선택하십시오', ['csv', 'json', 'xml'], 0);
```

### 프로그레스 바

대량 데이터 처리 중에 진행 상황을 알기 쉽게 표시할 수 있습니다.

```php theme={null}
use App\Models\Product;

// 컬렉션을 전달하기만 하면 자동으로 프로그레스 바 표시
$this->withProgressBar(Product::cursor(), function (Product $product) {
    $this->processProduct($product);
});
```

수동으로 세밀하게 제어하고 싶은 경우 다음 방법을 사용합니다.

```php theme={null}
$total = Product::count();
$bar = $this->output->createProgressBar($total);
$bar->start();

Product::cursor()->each(function (Product $product) use ($bar) {
    $this->processProduct($product);
    $bar->advance();
});

$bar->finish();
$this->newLine();
```

## 실용적인 유스케이스

### 데이터 임포트 명령어

CSV 파일에서 상품 데이터를 임포트하는 실전적인 명령어 예시입니다.

<Steps>
  <Step title="명령어 생성">
    ```shell theme={null}
    php artisan make:command ImportProducts
    ```
  </Step>

  <Step title="명령어 구현">
    ```php theme={null}
    <?php

    namespace App\Console\Commands;

    use App\Models\Product;
    use Illuminate\Console\Command;

    class ImportProducts extends Command
    {
        protected $signature = 'import:products
                                {file : CSV 파일의 경로}
                                {--limit= : 임포트할 최대 건수}
                                {--dry-run : 확인만(DB에 저장하지 않음)}';

        protected $description = 'CSV 파일에서 상품 데이터를 임포트합니다';

        public function handle(): void
        {
            $filePath = $this->argument('file');
            $limit    = $this->option('limit') ? (int) $this->option('limit') : null;
            $dryRun   = $this->option('dry-run');

            if (! file_exists($filePath)) {
                $this->error("파일을 찾을 수 없습니다: {$filePath}");
                return;
            }

            // PHP의 내장 함수로 CSV를 읽음
            $handle = fopen($filePath, 'r');
            $headers = fgetcsv($handle); // 1행째를 헤더로 취득
            $rows = [];
            while (($row = fgetcsv($handle)) !== false) {
                $rows[] = array_combine($headers, $row);
                if ($limit !== null && count($rows) >= $limit) {
                    break;
                }
            }
            fclose($handle);

            $count = 0;

            $this->withProgressBar($rows, function (array $row) use ($dryRun, &$count) {
                if (! $dryRun) {
                    Product::updateOrCreate(
                        ['sku' => $row['sku']],
                        [
                            'name'  => $row['name'],
                            'price' => $row['price'],
                        ]
                    );
                }
                $count++;
            });

            $this->newLine();

            if ($dryRun) {
                $this->warn("{$count}건이 대상입니다(드라이 런: 저장은 스킵했습니다)");
            } else {
                $this->info("{$count}건의 임포트가 완료되었습니다.");
            }
        }
    }
    ```
  </Step>

  <Step title="명령어 실행">
    ```shell theme={null}
    # 일반 임포트
    php artisan import:products storage/products.csv

    # 건수 제한
    php artisan import:products storage/products.csv --limit=100

    # 드라이 런으로 확인
    php artisan import:products storage/products.csv --dry-run
    ```
  </Step>
</Steps>

### 정기 유지보수 명령어

오래된 데이터의 삭제 등, 정기적으로 실행할 유지보수 명령어의 예시입니다.

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

namespace App\Console\Commands;

use App\Models\Order;
use Illuminate\Console\Command;

class PruneOldOrders extends Command
{
    protected $signature = 'orders:prune {--days=90 : 몇 일 이전보다 오래된 데이터를 삭제할지}';

    protected $description = '지정 일수보다 오래된 완료된 주문을 삭제합니다';

    public function handle(): void
    {
        $days = (int) $this->option('days');

        if (! $this->confirm("{$days}일 이상 전의 완료된 주문을 삭제하시겠습니까?")) {
            $this->info('취소했습니다.');
            return;
        }

        $deleted = Order::where('status', 'completed')
            ->where('created_at', '<', now()->subDays($days))
            ->delete();

        $this->info("{$deleted}건의 주문 데이터를 삭제했습니다.");
    }
}
```

### 스케줄 실행과의 조합

작성한 명령어는 `routes/console.php`에서 스케줄 실행할 수 있습니다.
자세한 내용은 [태스크 스케줄링](/ko/scheduling)을 참조하십시오.

```php theme={null}
use Illuminate\Support\Facades\Schedule;

// 매일 심야 2시에 실행
Schedule::command('orders:prune --days=90')->dailyAt('2:00');

// 매주 월요일 9시에 실행
Schedule::command('import:products storage/weekly.csv')->weeklyOn(1, '9:00');
```

## 명령어 테스트

Laravel의 테스트 기능을 사용해 Artisan 명령어의 동작을 검증할 수 있습니다.

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

namespace Tests\Feature\Commands;

use App\Models\Order;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PruneOldOrdersTest extends TestCase
{
    use RefreshDatabase;

    public function test_it_deletes_old_completed_orders(): void
    {
        // 90일 이상 전의 완료된 주문을 작성
        Order::factory()->create([
            'status'     => 'completed',
            'created_at' => now()->subDays(100),
        ]);

        // 최근 완료된 주문(삭제되지 않아야 함)
        Order::factory()->create([
            'status'     => 'completed',
            'created_at' => now()->subDays(10),
        ]);

        $this->artisan('orders:prune', ['--days' => 90])
            ->expectsConfirmation('90일 이상 전의 완료된 주문을 삭제하시겠습니까?', 'yes')
            ->expectsOutput('1건의 주문 데이터를 삭제했습니다.')
            ->assertExitCode(0);

        $this->assertDatabaseCount('orders', 1);
    }

    public function test_it_cancels_when_user_declines(): void
    {
        Order::factory()->create(['status' => 'completed', 'created_at' => now()->subDays(100)]);

        $this->artisan('orders:prune', ['--days' => 90])
            ->expectsConfirmation('90일 이상 전의 완료된 주문을 삭제하시겠습니까?', 'no')
            ->expectsOutput('취소했습니다.')
            ->assertExitCode(0);

        $this->assertDatabaseCount('orders', 1);
    }
}
```

<Info>
  `artisan()` 메서드에서 사용할 수 있는 어서션 메서드의 예시:

  | 메서드                               | 설명                     |
  | --------------------------------- | ---------------------- |
  | `expectsOutput('...')`            | 지정한 텍스트가 출력됨을 검증       |
  | `expectsQuestion('?', '답변')`      | `ask()`의 입력을 시뮬레이트     |
  | `expectsConfirmation('?', 'yes')` | `confirm()`의 응답을 시뮬레이트 |
  | `expectsChoice('?', '선택지')`       | `choice()`의 선택을 시뮬레이트  |
  | `assertExitCode(0)`               | 종료 코드를 검증(0 = 성공)      |
  | `assertFailed()`                  | 종료 코드가 0 이외임을 검증       |
</Info>

## 자주 사용하는 명령어 정리

```shell theme={null}
# 명령어를 신규 작성
php artisan make:command CommandName

# 사용 가능한 명령어 목록
php artisan list

# 명령어의 사용법 확인
php artisan help command:name

# Tinker 기동
php artisan tinker
```


## Related topics

- [Laravel Prompts](/ko/prompts.md)
- [프로세스](/ko/processes.md)
- [콘솔 테스트](/ko/console-tests.md)
- [요청 라이프사이클](/ko/lifecycle.md)
- [브라우저 테스트(Dusk)](/ko/dusk.md)
