Artisan 控制器

介绍

Artisan 是 Laravel 附带的命令行界面。Artisan 作为应用程序根目录下的 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 是一个强大的 REPL(交互式命令行环境),为 Laravel 框架提供支持,依赖于 PsySH 包。

安装

所有 Laravel 应用程序默认包含 Tinker。如果你之前将其从应用中移除,可以通过 Composer 重新安装 Tinker:

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::dispatchQueue::push 来调度作业。

命令允许列表

Tinker 使用 “允许” 列表来确定哪些 Artisan 命令可以在其 shell 中运行。默认情况下,你可以运行以下命令:clear-compileddownenvinspiremigratemigrate:installupoptimize。如果你希望允许更多的命令,可以将它们添加到 tinker.php 配置文件中的 commands 数组中:

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

不应别名化的类

通常,Tinker 会在你与它交互时自动别名化类。然而,你可能希望某些类永远不被别名化。你可以通过在 tinker.php 配置文件中的 dont_alias 数组列出这些类来实现:

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

编写命令

除了 Artisan 提供的命令,你还可以构建自己的自定义命令。命令通常存储在 app/Console/Commands 目录中;不过,你可以选择自己的存储位置,只要这些命令可以通过 Composer 加载即可。

生成命令

要创建一个新的命令,可以使用 make:command Artisan 命令。此命令将在 app/Console/Commands 目录下创建一个新的命令类。如果你的应用程序中没有这个目录,第一次运行 make:command Artisan 命令时,Laravel 会自动创建该目录:

php artisan make:command SendEmails

命令结构

在生成命令之后,你应该为类的 signaturedescription 属性定义适当的值。这些属性将在显示命令列表时使用。signature 属性还允许你定义命令的输入要求。handle 方法将在执行命令时被调用,你可以将命令的逻辑放在这个方法中。

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

<?php

namespace App\Console\Commands;

use App\Models\User;
use App\Support\DripEmailer;
use Illuminate\Console\Command;

class SendEmails extends Command
{
    /**
     * 控制台命令的名称和签名。
     *
     * @var string
     */
    protected $signature = 'mail:send {user}';

    /**
     * 控制台命令描述。
     *
     * @var string
     */
    protected $description = '向用户发送营销邮件';

    /**
     * 执行控制台命令。
     */
    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;

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

闭包命令描述

在定义基于闭包的命令时,你可以使用 purpose 方法为命令添加描述。此描述将在运行 php artisan listphp artisan help 命令时显示:

Artisan::command('mail:send {user}', function (string $user) {
    // ...
})->purpose('向用户发送营销邮件');

可隔离命令

要利用此功能,您的应用程序必须使用 memcachedredisdynamodbdatabasefilearray 缓存驱动作为应用程序的默认缓存驱动。此外,所有服务器必须与相同的中央缓存服务器进行通信。

有时您可能希望确保命令一次只运行一个实例。为此,您可以在命令类中实现 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 会使用命令的名称来生成用于获取原子锁的字符串键。您可以通过在 Artisan 命令类中定义 isolatableId 方法来自定义此键,使其集成命令的参数或选项:

/**
 * 获取命令的隔离 ID。
 */
public function isolatableId(): string
{
    return $this->argument('user');
}

锁过期时间

默认情况下,隔离锁在命令执行完成后会过期。如果命令被中断且无法完成,则锁将在一小时后过期。不过,您可以通过在命令中定义 isolationLockExpiresAt 方法来调整锁的过期时间:

use DateTimeInterface;
use DateInterval;

/**
 * 确定命令的隔离锁何时过期。
 */
public function isolationLockExpiresAt(): DateTimeInterface|DateInterval
{
    return now()->addMinutes(5);
}

定义输入期望

在编写控制台命令时,通常需要通过参数或选项从用户收集输入。Laravel 使得定义您期望从用户获取的输入非常方便,可以使用命令上的 signature 属性。signature 属性允许您使用一种简洁、类似路由的语法,在一个地方定义命令的名称、参数和选项。

参数

所有用户提供的参数和选项都被括在大括号 {} 中。在下面的示例中,命令定义了一个必需的参数:user

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

您还可以将参数设为可选,或者为参数定义默认值:

// 可选参数...
'mail:send {user?}'

// 带有默认值的可选参数...
'mail:send {user=foo}'

选项

选项与参数一样,是另一种用户输入的形式。选项在命令行中提供时以两个连字符(--)为前缀。选项有两种类型:接收值的选项和不接收值的选项。那些不接收值的选项充当布尔 “开关”。让我们看一个这种类型的选项的示例:

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

在这个示例中,--queue 选项可以在调用 Artisan 命令时指定。如果传递了 --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 的值设置为包含 12 的数组:

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 接口来配置命令,当缺少必需参数时自动提示用户:

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 方法,返回一个以参数名为键的问题数组:

/**
 * 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::where('name', 'like', "%{$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')
    ));
}

命令 I/O

检索输入

在命令执行期间,您可能需要访问命令所接受的参数和选项的值。为此,您可以使用 argumentoption 方法。如果参数或选项不存在,则会返回 null

/**
 * 执行控制台命令。
 */
public function handle(): void
{
    $userId = $this->argument('user');
}

如果需要以数组的形式检索所有参数,可以调用 arguments 方法:

$arguments = $this->arguments();

选项可以像参数一样轻松地通过 option 方法检索。要以数组形式检索所有选项,可以调用 options 方法:

// 检索特定的选项...
$queueName = $this->option('queue');

// 检索所有选项作为数组...
$options = $this->options();

提示输入

【Laravel Prompts】 是一个 PHP 包,用于在命令行应用程序中添加美观且用户友好的表单,提供类似浏览器的功能,包括占位符文本和验证。

除了显示输出,您还可以在执行命令时要求用户提供输入。ask 方法将提示用户输入给定的问题,接受用户的输入,然后将其返回到您的命令中:

/**
 * 执行控制台命令。
 */
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,但如果用户输入 yyes,则返回 true

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

如果需要,您还可以通过将 true 作为第二个参数传递给 confirm 方法,指定默认情况下确认提示应该返回 true

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

自动补全

anticipate 方法可用于为可能的选择提供自动补全。用户仍然可以提供任何答案,无论是否有自动补全提示:

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

另外,您还可以将一个闭包作为第二个参数传递给 anticipate 方法。每次用户输入字符时,闭包将被调用。闭包应接受一个字符串参数,包含用户迄今为止的输入,并返回自动补全的选项数组:

$name = $this->anticipate('What is your address?', function (string $input) {
    // 返回自动补全选项...
});

多项选择题

如果您需要给用户提供一个预定义的选项集来回答问题,可以使用 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
);

编写输出

要将输出发送到控制台,您可以使用 lineinfocommentquestionwarnerror 方法。每个方法将根据其用途使用适当的 ANSI 颜色。例如,下面我们将向用户显示一些通用信息。通常,info 方法将在控制台中以绿色文本显示:

/**
 * 执行控制台命令。
 */
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 方法显示空白行:

// 写一个空白行...
$this->newLine();

// 写三行空白...
$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;

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;

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

传递布尔值

如果您需要指定一个不接受字符串值的选项(例如 migrate:refresh 命令中的 --force 标志),您应该将 truefalse 作为选项的值传递:

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

排队 Artisan 命令

通过使用 Artisan 门面上的 queue 方法,您甚至可以将 Artisan 命令排入队列,以便它们由队列工作程序在后台处理。在使用此方法之前,请确保您已配置队列并正在运行队列监听器:

use Illuminate\Support\Facades\Artisan;

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

    // ...
});

使用 onConnectiononQueue 方法,您可以指定 Artisan 命令应被调度到的连接或队列:

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

从其它命令调用命令

有时您可能希望在现有的 Artisan 命令中调用其它命令。您可以使用 call 方法来实现。这个 call 方法接受命令名称和一组命令参数/选项:

/**
 * 执行控制台命令。
 */
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 方法:

/**
 * 执行控制台命令。
 */
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
});

存根自定义

Artisan 控制台的 make 命令用于创建各种类,例如控制器、任务、迁移和测试。这些类是通过“存根”文件生成的,这些存根文件会根据您的输入填充相应的值。然而,您可能希望对 Artisan 生成的文件进行一些小的修改。为此,您可以使用 stub:publish 命令将最常用的存根发布到您的应用程序中,以便您可以自定义它们:

php artisan stub:publish

发布的存根文件将位于应用程序根目录中的 stubs 目录内。您对这些存根文件所做的任何更改,在使用 Artisan 的 make 命令生成对应类时都会生效。

事件

Artisan 在运行命令时会触发三个事件:Illuminate\Console\Events\ArtisanStartingIlluminate\Console\Events\CommandStartingIlluminate\Console\Events\CommandFinished

  • ArtisanStarting 事件会在 Artisan 开始运行时立即触发。

  • CommandStarting 事件会在命令开始运行之前立即触发。

  • CommandFinished 事件会在命令执行完成后触发。