> ## 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 时，请以 `sail artisan` 取代 `php artisan` 执行。
  命令会在 Docker 容器内运行。

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

## Tinker

[Laravel Tinker](https://github.com/laravel/tinker) 是一个 REPL 环境，能在命令行中交互式操作整个应用，包括 Eloquent 模型、Job、事件等。

```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 会实际修改数据。在生产环境执行时请务必谨慎。
  它适合用于本地或 staging 环境的功能确认和调试。
</Tip>

## 创建自定义命令

### 生成命令

使用 `make:command` 生成命令类的骨架。

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

会生成 `app/Console/Commands/ImportProducts.php`。

### 命令的基本结构

生成的命令类包含 `$signature`、`$description`、`handle()` 三个主要要素。

```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 : 不真正写入数据库（仅确认）}';
```

| 语法                    | 类别      | 说明                      |
| --------------------- | ------- | ----------------------- |
| `{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');      // 布尔选项

    $this->info("文件：{$file}");

    if ($dryRun) {
        $this->warn('干运行模式：跳过写入数据库');
    }
}
```

### `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('确认导入到生产数据库？')) {
    $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 : 仅确认（不写入数据库）}';

        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); // 第一行为表头
            $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` 中安排调度执行。
详见[任务调度](/zh/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

- [控制台测试](/zh/console-tests.md)
- [教程 - Laravel Console Starter](/zh/packages/laravel-console-starter/tutorial.md)
- [InteractsWithTime trait](/zh/advanced/interacts-with-time.md)
- [Laravel Agent Detector — AI 智能体检测包](/zh/blog/agent-detector-introduction.md)
- [LINE SDK for Laravel](/zh/packages/laravel-line-sdk/index.md)
