laravel
5. 深入探讨
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,由 PsySH (opens in a new tab) 包提供支持。

安装

默认情况下,所有 Laravel 应用程序都包含 Tinker。但是,如果您之前从应用程序中删除了 Tinker,则可以使用 Composer 进行安装:

composer require laravel/tinker

[!NOTE]
在与您的 Laravel 应用程序交互时,是否正在寻找热重载、多行代码编辑和自动完成功能?请查看 Tinkerwell (opens in a new tab)!

使用

Tinker 允许您在命令行中与整个 Laravel 应用程序进行交互,包括您的 Eloquent 模型、任务、事件等。要进入 Tinker 环境,请运行 tinker Artisan 命令:

php artisan tinker

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

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

[!WARNING]
dispatch 辅助函数和 Dispatchable 类上的 dispatch 方法依赖于垃圾回收来将任务放入队列中。因此,在使用 tinker 时,您应该使用 Bus::dispatchQueue::push 来分发任务。

命令允许列表

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

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

不应别名的类

通常,当您在 Tinker 中与之交互时,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 命令时,它将被创建:

php artisan make:command SendEmails

命令结构

生成命令后,您应该为类的 signaturedescription 属性定义适当的值。当在 list 屏幕上显示您的命令时,将使用这些属性。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')));
    }
}

[!NOTE]
为了更好地重用代码,将控制台命令保持简洁并让它们依靠应用程序服务来完成其任务是一个好的实践。在上面的示例中,请注意我们注入了一个服务类来完成发送电子邮件的“繁重工作”。

退出代码

如果 handle 方法未返回任何内容且命令执行成功,则命令将以 0 退出代码退出,表示成功。但是,handle 方法可以选择返回一个整数来手动指定命令的退出代码:

$this->error('出了点问题。');

return 1;

如果您想从命令中的任何方法“使命令失败”,可以使用 fail 方法。fail 方法将立即终止命令的执行并返回 1 的退出代码:

$this->fail('出了点问题。');

闭包命令

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

尽管 routes/console.php 文件没有定义 HTTP 路由,但它定义了基于控制台的进入您的应用程序的入口点(路由)。在这个文件中,您可以使用 Artisan::command 方法定义所有基于闭包的控制台命令。command 方法接受两个参数:命令签名 和一个闭包,该闭包接收命令的参数和选项:

Artisan::command('mail:send {user}', function (string $user) {
    $this->info("正在向: {$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('向用户发送营销电子邮件');

可隔离命令

[!WARNING]
要使用此功能,您的应用程序必须使用 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

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

您还可以使参数可选或为参数定义默认值:

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

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

选项

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

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

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

php artisan mail:send 1 --queue

带值的选项

接下来,让我们看一个期望值的选项。如果用户必须为选项指定一个值,则应在选项名称后加上 = 符号:

/**
 * 控制台命令的名称和签名。
 *
 * @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 的值设置为一个包含 12 作为其值的数组:

php artisan mail:send 1 2

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

'mail:send {user?*}'

选项数组

当定义一个期望多个输入值的选项时,传递给命令的每个选项值都应以选项名称为前缀:

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

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

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

输入描述

您可以通过使用冒号将参数名称与描述分开来为输入参数和选项分配描述。如果您需要更多空间来定义您的命令,可以自由地将定义分布在多行上:

/**
 * 控制台命令的名称和签名。
 *
 * @var string
 */
protected $signature = 'mail:send
                        {user : 用户的 ID}
                        {--queue : 作业是否应排队}';

提示缺失的输入

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

<?php

namespace App\Console\Commands;

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

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

    //...
}

如果 Laravel 需要从用户那里收集必需的参数,它将通过使用参数名称或描述智能地措辞问题来自动询问用户。如果您希望自定义用于收集必需参数的问题,可以实现 promptForMissingArgumentsUsing 方法,返回一个以参数名称为键的问题数组:

/**
 * 使用返回的问题提示缺失的输入参数。
 *
 * @return array<string, string>
 */
protected function promptForMissingArgumentsUsing(): array
{
    return [
        'user' => '哪个用户 ID 应该接收邮件?',
    ];
}

您还可以通过使用包含问题和占位符的元组来提供占位符文本:

return [
    'user' => ['哪个用户 ID 应该接收邮件?', '例如 123'],
];

如果您希望完全控制提示,可以提供一个闭包,该闭包应该提示用户并返回他们的答案:

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

//...

return [
    'user' => fn () => search(
        label: '搜索用户:',
        placeholder: '例如 Taylor Otwell',
        options: fn ($value) => strlen($value) > 0
           ? User::where('name', 'like', "%{$value}%")->pluck('name', 'id')->all()
            : []
    ),
];

[!NOTE]
全面的 Laravel Prompts 文档包含有关可用提示及其用法的更多信息。

如果您希望提示用户选择或输入选项,您可以在命令的 handle 方法中包含提示。但是,如果您只想在用户也被自动提示缺少参数时提示用户,则可以实现 afterPromptingForMissingArguments 方法:

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

//...

/**
 * 在用户被提示缺少参数后执行操作。
 */
protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output): void
{
    $input->setOption('queue', confirm(
        label: '您是否想要将邮件排队?',
        default: $this->option('queue')
    ));
}

命令 I/O

检索输入

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

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

如果您需要将所有参数作为 array 检索,可以调用 arguments 方法:

$arguments = $this->arguments();

使用 option 方法可以像检索参数一样轻松地检索选项。要将所有选项作为数组检索,可以调用 options 方法:

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

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

提示输入

[!NOTE]
Laravel Prompts 是一个 PHP 包,用于为您的命令行应用程序添加美观且用户友好的表单,具有类似浏览器的功能,包括占位符文本和验证。

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

/**
 * 执行控制台命令。
 */
public function handle(): void
{
    $name = $this->ask('您叫什么名字?');

    //...
}

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

$name = $this->ask('您叫什么名字?', 'Taylor');

secret 方法与 ask 类似,但用户在控制台中输入时,他们的输入将不可见。当询问敏感信息(如密码)时,此方法很有用:

$password = $this->secret('密码是什么?');

询问确认

如果您需要向用户询问一个简单的“是或否”确认,您可以使用 confirm 方法。默认情况下,此方法将返回 false。但是,如果用户在提示中输入 yyes,该方法将返回 true

if ($this->confirm('您是否希望继续?')) {
    //...
}

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

if ($this->confirm('您是否希望继续?', true)) {
    //...
}

自动完成

anticipate 方法可用于为可能的选择提供自动完成功能。无论自动完成提示如何,用户仍然可以提供任何答案:

$name = $this->anticipate('您的名字是什么?', ['Taylor', 'Dayle']);

或者,您可以将一个闭包作为第二个参数传递给 anticipate 方法。每当用户输入一个字符时,该闭包将被调用。闭包应接受一个包含用户到目前为止输入的字符串参数,并返回一个用于自动完成的选项数组:

$name = $this->anticipate('您的地址是什么?', function (string $input) {
    // 返回自动完成选项...
});

多项选择题

如果您在提问时需要为用户提供一组预定义的选择,您可以使用 choice 方法。您可以通过将默认值的数组索引作为方法的第三个参数传递,来设置如果未选择任何选项时应返回的默认值的数组索引:

$name = $this->choice(
    '您的名字是什么?',
    ['Taylor', 'Dayle'],
    $defaultIndex
);

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

$name = $this->choice(
    '您的名字是什么?',
    ['Taylor', 'Dayle'],
    $defaultIndex,
    $maxAttempts = null,
    $allowMultipleSelections = false
);

输出内容

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

/**
 * 执行控制台命令。
 */
public function handle(): void
{
    //...

    $this->info('该命令已成功!');
}

要显示错误消息,请使用 error 方法。错误消息文本通常以红色显示:

$this->error('出了点问题!');

您可以使用 line 方法显示普通的、未着色的文本:

$this->line('在屏幕上显示此内容');

您可以使用 newLine 方法显示一个空行:

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

// 写入三个空行...
$this->newLine(3);

表格

table 方法可以轻松地正确格式化多行/多列数据。您只需要提供列名和表格的数据,Laravel 将自动为您计算表格的适当宽度和高度:

use App\Models\User;

$this->table(
    ['姓名', '电子邮件'],
    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 进度条组件文档 (opens in a new tab)

注册命令

默认情况下,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\CommandFinishedArtisanStarting 事件在 Artisan 开始运行时立即分发。接下来,CommandStarting 事件在命令运行之前立即分发。最后,CommandFinished 事件在命令执行完成后分发。