> ## 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 Console

> 說明如何使用 Laravel 的 Artisan Console 建立自訂 CLI 指令。

## 什麼是 Artisan

Artisan 是 Laravel 隨附的命令列介面（CLI）工具。
它以專案根目錄的 `artisan` script 存在，提供多個能有效率地進行開發、運維的指令。

要確認可用指令清單，使用 `list` 指令。

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

要確認指令的用法，於前面加上 `help`。

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

<Info>
  若使用 Laravel Sail，請以 `sail artisan` 代替 `php 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)
// 以 factory 建立紀錄
>>> App\Models\User::factory()->create()
// 直接呼叫服務類別
>>> app(App\Services\OrderService::class)->process(1)
```

<Tip>
  Tinker 會實際變更資料。於正式環境執行請充分留意。
  適合本地或 stg 環境的行為確認、除錯用途。
</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 Attribute 的寫法。

  ```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('Dry Run 模式：略過對 DB 的儲存');
    }
}
```

### `input()` — 型別化的存取子

使用 `input()` 方法，可以與 HTTP Request 相同的型別化存取子取得指令的引數或選項。

```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>

## 與使用者的互動

### 訊息的輸出

備有輸出彩色訊息至 Console 的方法。

```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 Key');

// Yes/No 確認
if (! $this->confirm('是否匯入至正式 DB？')) {
    $this->info('已取消。');
    return;
}

// 從選項選擇
$format = $this->choice('請選擇輸出格式', ['csv', 'json', 'xml'], 0);
```

### 進度條

於大量資料處理中可清楚顯示進度。

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

// 只要傳入 Collection 即自動顯示進度條
$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 行作為 header
            $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} 件為對象（Dry Run：已略過儲存）");
            } 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

    # 以 Dry Run 確認
    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` 排程執行。
詳情請參考[Task Scheduling](/zh-TW/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>

## `dev` 指令

`dev` Artisan 指令將本地開發所需的多個 Process 於單一終端機視窗一併啟動。預設會平行執行 PHP 開發伺服器、Queue Worker、[Pail](/zh-TW/logging) 日誌監視、Vite 資產編譯。

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

內部使用名為 `concurrently` 的 npm 套件管理 Process。各 Process 會被標示標籤與顏色區分，於終端機輸出可輕易辨識。若任一 Process 失敗，其他所有 Process 也會自動停止。

預設執行的 Process 如下。

| 名稱       | 指令                                               |
| -------- | ------------------------------------------------ |
| `server` | `php artisan serve --host=localhost`             |
| `queue`  | `php artisan queue:listen --tries=1 --timeout=0` |
| `logs`   | `php artisan pail --timeout=0`                   |
| `vite`   | `npm run dev`                                    |

<Info>
  `vite` process 會自動偵測所使用的 Node 套件管理器（npm、pnpm、Yarn、Bun）並使用適當的執行指令。
</Info>

### `dev` process 的自訂

`dev` 指令執行的 Process 可以透過 `DevCommands` 類別自訂。通常於應用程式 `AppServiceProvider` 的 `boot` 方法內註冊。`register` 方法接收指令字串與任意的 Process 名稱。

```php theme={null}
use Illuminate\Foundation\DevCommands;

/**
 * 應用程式服務的初始啟動處理
 */
public function boot(): void
{
    DevCommands::register('some-command --flag', 'my-process');
}
```

要註冊 Artisan 指令時，可使用會自動前置 `php artisan` 的 `artisan` 方法。

```php theme={null}
DevCommands::artisan('horizon', 'horizon');
```

同樣地，`node` 方法會前置偵測到之套件管理器的執行指令（如 `npm run`），`nodeExec` 方法會前置套件管理器的 exec 指令（如 `npx`）。

```php theme={null}
DevCommands::node('storybook', 'storybook');

DevCommands::nodeExec('tailwindcss -i resources/css/app.css -o public/css/app.css --watch', 'tailwind');
```

若以與預設 Process 相同名稱註冊 Process，會替換該預設 Process。例如可自訂 server process 使用其他 port。

```php theme={null}
DevCommands::artisan('serve --host=localhost --port=9000', 'server');
```

終端機中 Process 標籤的顏色也可自訂。可用的顏色方法有 `blue`、`purple`、`pink`、`orange`、`green`、`yellow`。也可對 `color` 方法傳入自訂的 hex 色碼。

```php theme={null}
DevCommands::register('my-command', 'my-process')->green();

DevCommands::register('my-command', 'my-process')->color('#ff6347');
```

若希望列出已註冊的 `dev` process 而不實際啟動，可使用 `dev:list` 指令。

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

### `dev` process 的過濾

使用 `only` 方法可指定於執行 `dev` 指令時僅啟動特定 Process。同樣地，可用 `except` 方法排除特定 Process。

```php theme={null}
// 僅啟動 server 與 vite process...
DevCommands::only('server', 'vite');

// 啟動 Queue Worker 以外的所有 Process...
DevCommands::except('queue');
```

<Tip>
  若於本地開發併用 Horizon 或 Reverb 等，透過 `DevCommands::artisan()` 註冊後，即可以單一 `php artisan dev` 一次啟動所有 Process 非常方便。
</Tip>

## 常用指令彙整

```shell theme={null}
# 新建指令
php artisan make:command CommandName

# 可用指令清單
php artisan list

# 確認指令用法
php artisan help command:name

# 啟動 Tinker
php artisan tinker
```


## Related topics

- [Laravel Prompts](/zh-TW/prompts.md)
- [Process](/zh-TW/processes.md)
- [延遲服務提供者](/zh-TW/advanced/deferred-provider.md)
- [Laravel Console Starter](/zh-TW/packages/laravel-console-starter/index.md)
- [教學 - Laravel Console Starter](/zh-TW/packages/laravel-console-starter/tutorial.md)
