Artisan 命令

简介

Artisan 是 Laravel 自带的命令行界面。它以 artisan 脚本的形式存在于应用根目录,提供了很多实用命令来辅助开发。查看所有可用的 Artisan 命令,用 list 命令:

php artisan list

每个命令都有一个「帮助」页面,展示并描述该命令可用的参数和选项。查看帮助页面,在命令名前加 help:

php artisan help migrate

Laravel Sail

如果你用 Laravel Sail 作为本地开发环境,记得用 sail 命令行来执行 Artisan 命令。Sail 会在应用的 Docker 容器里执行你的 Artisan 命令:

./vendor/bin/sail artisan list

Tinker(REPL)

Laravel Tinker 是 Laravel 框架的一个强大的 REPL(交互式命令行,Read-Eval-Print Loop),基于 PsySH 包构建。

安装

所有 Laravel 应用默认都包含 Tinker。不过,如果你之前移除了它,可以用 Composer 重新安装:

composer require laravel/tinker

信息

想在交互式操作 Laravel 应用时获得热重载、多行编辑和自动补全?试试 Tinkerwell。

用法

Tinker 让你在命令行上与整个 Laravel 应用交互,包括 Eloquent 模型、任务、事件等。进入 Tinker 环境,运行 tinker Artisan 命令:

php artisan tinker

可以用 vendor:publish 命令发布 Tinker 的配置文件:

php artisan vendor:publish --provider="Laravel\Tinker\TinkerServiceProvider"

注意

dispatch 辅助函数和 Dispatchable 类上的 dispatch() 方法依赖垃圾回收来将任务放入队列。因此在 Tinker 中,你应该用 Bus::dispatch 或 Queue::push 来分发任务。

命令白名单

Tinker 用一个「白名单」来决定哪些 Artisan 命令可以在它的 shell 中运行。默认情况下,你可以运行 clear-compiled、down、env、inspire、migrate、migrate:install、up 和 optimize 命令。如果想允许更多命令,可以把它们加到 tinker.php 配置文件的 commands 数组里:

'commands' => [
    // App\Console\Commands\ExampleCommand::class,
],

不应被别名的类

通常,Tinker 会在你交互时自动为类创建别名。但有些类你可能不希望被别名。可以在 tinker.php 配置文件的 dont_alias 数组里列出这些类:

'dont_alias' => [
    App\Models\User::class,
],

编写命令

除了 Artisan 自带的命令,你还可以构建自己的自定义命令。命令通常放在 app/Console/Commands 目录;不过你可以自由选择存放位置,只要告诉 Laravel 扫描其他目录即可。

生成命令

创建新命令,用 make:command Artisan 命令。这个命令会在 app/Console/Commands 目录下创建一个新的命令类。如果这个目录不存在,第一次运行 make:command 时会自动创建:

php artisan make:command SendEmails

命令结构

生成命令后,用 Signature 和 Description 属性定义命令的签名和描述。Signature 属性还允许你定义命令的输入预期。handle() 方法会在命令执行时被调用,你可以把命令逻辑放在这个方法里。

来看一个示例命令。注意,我们可以通过命令的 handle() 方法请求任何需要的依赖。Laravel 服务容器会自动注入方法签名中类型提示的所有依赖:

<?php

namespace App\Console\Commands;

use App\Models\User;
use App\Support\DripEmailer;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;

#[Signature('mail:send {user}')]
#[Description('Send a marketing email to a user')]
class SendEmails extends Command
{
    /**
     * Execute the console command.
     */
    public function handle(DripEmailer $drip): void
    {
        $drip->send(User::find($this->argument('user')));
    }
}

信息

为了更好的代码复用,建议保持控制台命令轻量,把实际工作交给应用服务来完成。上面的示例中,我们注入了一个服务类来承担发送邮件的「重活」。

退出码

如果 handle() 方法没有返回值且命令执行成功,命令会以 0 退出码结束,表示成功。不过,handle() 方法可以选择返回一个整数来手动指定退出码:

$this->error('Something went wrong.');

return 1;

如果你想从命令中的任何方法「中止」命令,可以用 fail() 方法。fail() 方法会立即终止命令执行并返回退出码 1:

$this->fail('Something went wrong.');

闭包命令

基于闭包的命令提供了一种替代类定义命令的方式。就像路由闭包是控制器的替代方案一样,命令闭包可以看作命令类的替代方案。

routes/console.php 文件虽然不定义 HTTP 路由,但它定义了应用的基于命令行的入口点(路由)。在这个文件里,你可以用 Artisan::command 方法定义所有基于闭包的控制台命令。command() 方法接收两个参数:命令签名和接收命令参数与选项的闭包:

Artisan::command('mail:send {user}', function (string $user) {
    $this->info("Sending email to: {$user}!");
});

闭包绑定到底层的命令实例,所以你可以访问完整命令类上所有可用的辅助方法。

类型提示依赖

除了接收命令的参数和选项,命令闭包还可以类型提示需要从服务容器解析的额外依赖:

use App\Models\User;
use App\Support\DripEmailer;
use Illuminate\Support\Facades\Artisan;

Artisan::command('mail:send {user}', function (DripEmailer $drip, string $user) {
    $drip->send(User::find($user));
});

闭包命令描述

定义闭包命令时,可以用 purpose() 方法为命令添加描述。运行 php artisan list 或 php artisan help 时会显示这个描述:

Artisan::command('mail:send {user}', function (string $user) {
    // ...
})->purpose('Send a marketing email to a user');

隔离命令

注意

要使用这个功能,你的应用必须使用 memcached、redis、dynamodb、database、file 或 array 缓存驱动作为默认缓存驱动。此外,所有服务器必须连接同一个中央缓存服务器。

有时候你可能需要确保一个命令同一时间只能运行一个实例。为此,可以在命令类上实现 Illuminate\Contracts\Console\Isolatable 接口:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Contracts\Console\Isolatable;

class SendEmails extends Command implements Isolatable
{
    // ...
}

当命令标记为 Isolatable 后,Laravel 会自动为该命令提供 --isolated 选项,无需在命令选项中显式定义。使用该选项调用命令时,Laravel 会确保没有其他该命令的实例正在运行。Laravel 通过应用的默认缓存驱动尝试获取原子锁来实现这一点。如果其他实例正在运行,命令不会执行;但命令仍会以成功的退出状态码结束:

php artisan mail:send 1 --isolated

如果想指定命令无法执行时返回的退出状态码,可以通过 isolated 选项传入:

php artisan mail:send 1 --isolated=12

锁 ID

默认情况下,Laravel 会用命令名生成用于获取缓存原子锁的字符串 key。不过,你可以在 Artisan 命令类上定义 isolatableId() 方法来自定义这个 key,把命令的参数或选项集成到 key 中:

/**
 * Get the isolatable ID for the command.
 */
public function isolatableId(): string
{
    return $this->argument('user');
}

锁过期时间

默认情况下,隔离锁在命令结束后过期。如果命令被中断无法完成,锁会在一小时后过期。不过,你可以在命令上定义 isolationLockExpiresAt() 方法来调整锁的过期时间:

use DateTimeInterface;
use DateInterval;

/**
 * Determine when an isolation lock expires for the command.
 */
public function isolationLockExpiresAt(): DateTimeInterface|DateInterval
{
    return now()->plus(minutes: 5);
}

定义输入

编写控制台命令时,通常需要通过参数或选项从用户那里获取输入。Laravel 让你用命令的 signature 属性非常方便地定义预期的输入。signature 属性允许你用一种类似路由的、富有表现力的语法在单个字符串中定义命令的名称、参数和选项。

参数

所有用户提供的参数和选项都用花括号包裹。下面的示例中,命令定义了一个必填参数 user:

/**
 * The name and signature of the console command.
 *
 * @var string
 */
protected $signature = 'mail:send {user}';

你也可以将参数设为可选的,或为参数定义默认值:

// Optional argument...
'mail:send {user?}'

// Optional argument with default value...
'mail:send {user=foo}'

选项

选项和参数一样,是另一种用户输入形式。选项在命令行中以前缀两个连字符(--)提供。选项分两种:接收值的和不接收值的。不接收值的选项充当布尔「开关」。来看一个这类选项的示例:

/**
 * The name and signature of the console command.
 *
 * @var string
 */
protected $signature = 'mail:send {user} {--queue}';

这个示例中,调用 Artisan 命令时可以指定 --queue 开关。如果传了 --queue,选项的值为 true,否则为 false:

php artisan mail:send 1 --queue

带值的选项

接下来看一个需要值的选项。如果用户必须为选项指定值,应该在选项名后加一个 = 号:

/**
 * The name and signature of the console command.
 *
 * @var string
 */
protected $signature = 'mail:send {user} {--queue=}';

这个示例中,用户可以这样为选项传值。如果调用命令时没有指定该选项,其值为 null:

php artisan mail:send 1 --queue=default

你可以在选项名后指定默认值来为选项赋默认值。如果用户没有传选项值,就使用默认值:

'mail:send {user} {--queue=default}'

选项快捷方式

定义选项时想指定快捷方式,可以在选项名前加上快捷方式,用 | 分隔快捷方式和完整选项名:

'mail:send {user} {--Q|queue=}'

在终端调用命令时,选项快捷方式应以单个连字符为前缀,指定值时不要包含 =:

php artisan mail:send 1 -Qdefault

输入数组

如果想定义参数或选项接收多个输入值,可以用 * 字符。先看一个指定这种参数的示例:

'mail:send {user*}'

运行这个命令时,user 参数可以按顺序在命令行中传入。例如,下面的命令会把 user 的值设为包含 1 和 2 的数组:

php artisan mail:send 1 2

* 字符可以和可选参数定义结合,允许零个或多个参数实例:

'mail:send {user?*}'

选项数组

定义接收多个输入值的选项时,传给命令的每个选项值都应带有选项名前缀:

'mail:send {--id=*}'

这样的命令可以通过传递多个 --id 参数来调用:

php artisan mail:send --id=1 --id=2

输入描述

你可以用冒号分隔参数名和描述来为输入参数和选项分配描述。如果需要更多空间来定义命令,可以把定义分散到多行:

/**
 * The name and signature of the console command.
 *
 * @var string
 */
protected $signature = 'mail:send
                        {user : The ID of the user}
                        {--queue : Whether the job should be queued}';

提示缺失的输入

如果你的命令包含必填参数,用户未提供时会收到错误消息。或者,你可以通过实现 PromptsForMissingInput 接口来配置命令在缺少必填参数时自动提示用户:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;

class SendEmails extends Command implements PromptsForMissingInput
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'mail:send {user}';

    // ...
}

如果 Laravel 需要从用户那里获取必填参数,它会自动利用参数名或描述智能提问。如果你想自定义用于获取必填参数的问题,可以实现 promptForMissingArgumentsUsing() 方法,返回一个以参数名为 key 的问题数组:

/**
 * Prompt for missing input arguments using the returned questions.
 *
 * @return array<string, string>
 */
protected function promptForMissingArgumentsUsing(): array
{
    return [
        'user' => 'Which user ID should receive the mail?',
    ];
}

你还可以用包含问题和占位文本的元组来提供占位提示:

return [
    'user' => ['Which user ID should receive the mail?', 'E.g. 123'],
];

如果想完全控制提示,可以提供一个闭包来提示用户并返回他们的答案:

use App\Models\User;
use function Laravel\Prompts\search;

// ...

return [
    'user' => fn () => search(
        label: 'Search for a user:',
        placeholder: 'E.g. Taylor Otwell',
        options: fn ($value) => strlen($value) > 0
            ? User::whereLike('name', "%{$value}%")->pluck('name', 'id')->all()
            : []
    ),
];

信息

Laravel Prompts 的完整文档包含了可用提示及其用法的更多信息。

如果你想在用户被自动提示缺失参数的同时提示用户选择或输入选项,可以在命令的 handle() 方法中加入提示。不过,如果你只想在用户已经被自动提示缺失参数时才提示,可以实现 afterPromptingForMissingArguments() 方法:

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function Laravel\Prompts\confirm;

// ...

/**
 * Perform actions after the user was prompted for missing arguments.
 */
protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output): void
{
    $input->setOption('queue', confirm(
        label: 'Would you like to queue the mail?',
        default: $this->option('queue')
    ));
}

命令输入输出

获取输入

命令执行时,你可能需要访问命令接受的参数和选项的值。为此,可以用 argument() 和 option() 方法。如果参数或选项不存在,返回 null:

/**
 * Execute the console command.
 */
public function handle(): void
{
    $userId = $this->argument('user');
}

如果需要以数组形式获取所有参数,调用 arguments() 方法:

$arguments = $this->arguments();

选项的获取和参数一样简单,用 option() 方法。以数组形式获取所有选项,调用 options() 方法:

// Retrieve a specific option...
$queueName = $this->option('queue');

// Retrieve all options as an array...
$options = $this->options();

你可以用 input() 方法获取命令的参数和选项作为 Illuminate\Console\CommandInput 实例,它提供了和 HTTP 请求及其他数据容器相同的类型化访问器:

use App\Enums\ReportType;

/**
 * Execute the console command.
 */
public function handle(): void
{
    $input = $this->input()->date('from');

    // ...
}

input() 方法也可以用于从参数或选项中获取单个输入值:

$queue = $this->input('queue', 'default');

提示输入

信息

Laravel Prompts 是一个 PHP 包,用于在命令行应用中添加美观且用户友好的表单,具有占位文本和验证等浏览器般的特性。

除了显示输出,你还可以在命令执行过程中要求用户提供输入。ask() 方法会用给定的问题提示用户,接收输入,然后将用户的输入返回给命令:

/**
 * Execute the console command.
 */
public function handle(): void
{
    $name = $this->ask('What is your name?');

    // ...
}

ask() 方法还接受一个可选的第二参数,指定用户未提供输入时返回的默认值:

$name = $this->ask('What is your name?', 'Taylor');

secret() 方法类似 ask(),但用户在控制台输入时不会看到输入内容。这个方法在询问密码等敏感信息时很有用:

$password = $this->secret('What is the password?');

确认

如果需要向用户确认简单的「是或否」,可以用 confirm() 方法。默认情况下,这个方法返回 false。但如果用户输入 y 或 yes,方法会返回 true:

if ($this->confirm('Do you wish to continue?')) {
    // ...
}

如有需要,可以通过给 confirm() 方法传 true 作为第二参数来让确认提示默认返回 true:

if ($this->confirm('Do you wish to continue?', true)) {
    // ...
}

自动补全

anticipate() 方法可以用于为可能的选择提供自动补全。用户仍然可以提供任何答案,不受自动补全提示的限制:

$name = $this->anticipate('What is your name?', ['Taylor', 'Dayle']);

或者,你可以传一个闭包作为 anticipate() 方法的第二参数。用户每次输入一个字符时都会调用这个闭包。闭包应接收一个包含用户当前输入的字符串参数,并返回一个自动补全选项数组:

use App\Models\Address;

$name = $this->anticipate('What is your address?', function (string $input) {
    return Address::whereLike('name', "{$input}%")
        ->limit(5)
        ->pluck('name')
        ->all();
});

多选问题

如果提问时需要给用户一组预定义的选择,可以用 choice() 方法。可以通过第三参数指定未选择选项时返回的默认值的数组索引:

$name = $this->choice(
    'What is your name?',
    ['Taylor', 'Dayle'],
    $defaultIndex
);

此外,choice() 方法接受可选的第四和第五参数,分别用于确定选择有效响应的最大尝试次数以及是否允许多选:

$name = $this->choice(
    'What is your name?',
    ['Taylor', 'Dayle'],
    $defaultIndex,
    $maxAttempts = null,
    $allowMultipleSelections = false
);

输出

要向控制台输出,可以用 line()、newLine()、info()、comment()、question()、warn()、alert() 和 error() 方法。这些方法都会根据各自用途使用适当的 ANSI 颜色。比如显示一些常规信息,info() 方法通常在控制台中显示为绿色文本:

/**
 * Execute the console command.
 */
public function handle(): void
{
    // ...

    $this->info('The command was successful!');
}

显示错误消息,用 error() 方法。错误消息文本通常显示为红色:

$this->error('Something went wrong!');

用 line() 方法显示不带颜色的纯文本:

$this->line('Display this on the screen');

用 newLine() 方法显示空行:

// Write a single blank line...
$this->newLine();

// Write three blank lines...
$this->newLine(3);

表格

table() 方法让正确格式化多行/多列数据变得简单。你只需提供列名和数据,Laravel 会自动计算表格的适当宽度和高度:

use App\Models\User;

$this->table(
    ['Name', 'Email'],
    User::all(['name', 'email'])->toArray()
);

进度条

对于长时间运行的任务,显示一个进度条来告知用户任务完成进度会很有帮助。用 withProgressBar() 方法,Laravel 会显示进度条并在每次迭代给定可迭代值时推进进度:

use App\Models\User;

$users = $this->withProgressBar(User::all(), function (User $user) {
    $this->performTask($user);
});

有时你可能需要更手动地控制进度条的推进。首先定义流程将迭代的总步数,然后在处理每个项目后推进进度条:

$users = App\Models\User::all();

$bar = $this->output->createProgressBar(count($users));

$bar->start();

foreach ($users as $user) {
    $this->performTask($user);

    $bar->advance();
}

$bar->finish();

信息

更多高级选项,查看 Symfony Progress Bar 组件文档。

注册命令

默认情况下,Laravel 会自动注册 app/Console/Commands 目录下的所有命令。不过,你可以在 bootstrap/app.php 中用 withCommands 方法让 Laravel 扫描其他目录中的 Artisan 命令:

->withCommands([
    __DIR__.'/../app/Domain/Orders/Commands',
])

如有需要,你也可以通过向 withCommands 方法提供命令类名来手动注册命令:

use App\Domain\Orders\Commands\SendEmails;

->withCommands([
    SendEmails::class,
])

Artisan 启动时,应用中的所有命令都会由服务容器解析并注册到 Artisan。

以编程方式执行命令

有时你可能需要在 CLI 之外执行 Artisan 命令。比如,你可能想从路由或控制器中执行 Artisan 命令。可以用 Artisan 门面的 call() 方法。call() 方法接收命令的签名名或类名作为第一参数,命令参数数组作为第二参数。返回退出码:

use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Route;

Route::post('/user/{user}/mail', function (string $user) {
    $exitCode = Artisan::call('mail:send', [
        'user' => $user, '--queue' => 'default'
    ]);

    // ...
});

或者,你可以把整个 Artisan 命令作为字符串传给 call() 方法:

Artisan::call('mail:send 1 --queue=default');

传递数组值

如果你的命令定义了接收数组的选项,可以给该选项传递一个值数组:

use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Route;

Route::post('/mail', function () {
    $exitCode = Artisan::call('mail:send', [
        '--id' => [5, 13]
    ]);
});

传递布尔值

如果需要为不接收字符串值的选项指定值,比如 migrate:refresh 命令的 --force 标志,应该传 true 或 false 作为选项值:

$exitCode = Artisan::call('migrate:refresh', [
    '--force' => true,
]);

队列化 Artisan 命令

用 Artisan 门面的 queue() 方法,你甚至可以把 Artisan 命令放入队列,让队列 worker在后台处理。使用此方法前,确保已配置队列并正在运行队列监听器:

use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Route;

Route::post('/user/{user}/mail', function (string $user) {
    Artisan::queue('mail:send', [
        'user' => $user, '--queue' => 'default'
    ]);

    // ...
});

用 onConnection() 和 onQueue() 方法,可以指定 Artisan 命令分发到的连接或队列:

Artisan::queue('mail:send', [
    'user' => 1, '--queue' => 'default'
])->onConnection('redis')->onQueue('commands');

从其他命令调用命令

有时你可能想从现有 Artisan 命令中调用其他命令。可以用 call() 方法。这个 call() 方法接收命令名和命令参数/选项数组:

/**
 * Execute the console command.
 */
public function handle(): void
{
    $this->call('mail:send', [
        'user' => 1, '--queue' => 'default'
    ]);

    // ...
}

如果你想调用另一个控制台命令并屏蔽其所有输出,可以用 callSilently() 方法。callSilently() 方法的签名和 call() 方法相同:

$this->callSilently('mail:send', [
    'user' => 1, '--queue' => 'default'
]);

信号处理

操作系统允许向运行中的进程发送信号。比如,SIGTERM 信号是操作系统要求程序优雅终止的方式。如果你想在 Artisan 控制台命令中监听信号并在信号发生时执行代码,可以用 trap() 方法:

/**
 * Execute the console command.
 */
public function handle(): void
{
    $this->trap(SIGTERM, fn () => $this->shouldKeepRunning = false);

    while ($this->shouldKeepRunning) {
        // ...
    }
}

要同时监听多个信号,可以给 trap() 方法传一个信号数组:

$this->trap([SIGTERM, SIGQUIT], function (int $signal) {
    $this->shouldKeepRunning = false;

    dump($signal); // SIGTERM / SIGQUIT
});

Stub 定制

Artisan 控制台的 make 命令用于创建各种类,如控制器、任务、迁移和测试。这些类使用「stub」文件生成,根据你的输入填充值。不过,你可能想对 Artisan 生成的文件做些小改动。为此,可以用 stub:publish 命令将最常用的 stub 发布到应用中,以便自定义:

php artisan stub:publish

发布后的 stub 会放在应用根目录的 stubs 目录中。你对这些 stub 做的任何修改都会在使用 Artisan 的 make 命令生成对应类时生效。

事件

Artisan 运行命令时会分发三个事件:Illuminate\Console\Events\ArtisanStarting、Illuminate\Console\Events\CommandStarting 和 Illuminate\Console\Events\CommandFinished。ArtisanStarting 事件在 Artisan 开始运行时立即分发。接下来,CommandStarting 事件在命令运行前立即分发。最后,CommandFinished 事件在命令执行完成后分发。