Laravel 实用小技巧 —— Artisan 入门(下)

在上篇文章《 Laravel 实战小技巧 —— Artisan 入门(上)》 里,我们介绍了自定义命令创建方式、签名的定义,这篇文章我们重点为大家介绍命令的输入输出相关的内容。

输入逻辑

介绍完定义命令结构的方式,我们再来看一下命令逻辑的实现部分。

首先是获取输入的逻辑。获取输入有两种方式,一种是从命令行中直接获取输入,一种是通过命令行交互的方式获取输入。

命令行直接输入

获取方式 说明
$this->argument('{参数名称}') 获取指定参数
$this->arguments() 获取所有参数
$this->option('{选项名称}') 获取指定选项
$this->options() 获取所有选项

命令行交互输入

交互方式 说明
$this->ask('{询问信息提示}') 以普通输入形式获取输入内容
$this->secret('{密码信息提示}') 以密文形式获取输入内容
$this->confirm('{确认信息提示}') 获取用户确认信息输入,yyes 返回 true,其他输入返回 false,不区分大小写
$this->anticipate('{自动补全信息提示}', ['{提示信息}']) 获取用户输入内容,并根据用户输入提供自动补全信息提示
$this->choice('{选择信息提示}', ['{选项}']) 给用户提供多个选项,用户只能输入合法选项的索引值或选项值,支持多选配置

前三种交互方式比较简单,我们不做过多介绍,这里我们再单独说一下 anticipatechoice 两个方法。

anticipate 方法

anticipate 这个方法除了提供常规的补全配置外,还提供了闭包的传参方式,如下:

$name = $this->anticipate('{自动补全提示信息}', function ($input) {
    // 返回自动完成配置
});

这就比较灵活了,我们可以在闭包中实现我们自定义的补全逻辑。比如当我们需要用户输入地区信息时,就可以在闭包中根据用户实时输入的信息匹配可能的地址信息,然后作为提示数组返回给前端。

注意: 闭包中返回的必须是数组或者实现了 Countable 接口的对象。

choice 方法

choice 方法包含了五个参数,具体描述如下:

$name = $this->choice(
    '{提示信息}',
    ['{选项1}', '{选项2}'],
    $defaultIndex = null, //默认索引
    $maxAttempts = null, //最大尝试次数
    $allowMultipleSelections = false //是否支持多选
);
  • defaultIndex: 如果指定了默认索引的话,当用户在命令行直接按 Enter 键提交时,获取到的就是默认索引对应的值。没有指定默认索引时,直接 Enter 提交会报错。
  • maxAttempts: 如果设置了最大尝试次数的话,当用户在失败次数达到 maxAttempts 次以后,会退出交互界面。默认情况下不会退出。
  • allowMultipleSelections: 如果设置了支持多选的话,可以通过输入选项一,选项二[选项三...] 的方式同时提交多个选项,选项之间通过英文逗号分隔。如果多个选项中存在错误的选项,则会抛出错误。

choice 有个特殊的地方,让我们来看看下面这段代码:

$choice = $this->choice('请选择你的数字', ['2', '1', '0']);

运行命令后,交互界面显示如下:

Ntv4051SMh.png

比如我们想选择 0 ,输入对应的索引 2,这时如果我们在 handle 中获取 $choice 会不会是我们预期的 0 呢?

并不是。结果输出的居然是 2 !这是为什么呢?

我们通过查看源代码可以发现,原因就在以下给出的这段代码逻辑中(这里选取重点片段讲解):

代码: ./vendor/symfony/console/Question/ChoiceQuestion.php

...
$result = array_search($value, $choices); // 查找选项数组中是否存在提交的「选项」

// 非关联数组处理
if (!$isAssoc) {
    // 存在值的情况返回对应的值
    if (false !== $result) {
        $result = $choices[$result];
    // 不存在对应值的情况判断是否存在对应的键,存在则返回键对应的值
    } elseif (isset($choices[$value])) {
        $result = $choices[$value];
    }
//关联数组处理
} elseif (false === $result && isset($choices[$value])) {
    // 不存在值的情况下判断是否存在对应的键,如果存在,返回对应的值
    $result = $value;
}
...

从上述代码中可以看出,当选项数组是非关联数组时,会先检查输入的选项是否是选项数组的值,如果是的话,则直接将其返回,然后才会检查是否是选项数组的键。这也就解释了为什么会有上面例子中那个结果了。

输出逻辑

文本输出

我们可以使用 lineinfocommentquestionerror 方法,输出文本到控制台。显示效果如下:

9Mq1gB3HG4.png!large

通过 newLine($count) 方法可以创建 $count行的空行,$count 默认为 1 。

表格输出

通过 table 方法可以创建一个表格,代码如下:

$this->table(
    // 表头
    ['姓名', '性别', '年龄'],
    // 表格内容
    [
        ['张三', '男', 20],
        ['李四', '男', 30],
        ['王五', '男', 40],
    ],
    // 表格样式,支持的样式:'default', 'borderless', 'compact', 'symfony-style-guide', 'box', 'box-double'
    // $tableStyle = 'default',
    // 列样式
    // $columnStyles = [],
);

各种不同样式的表格显示效果如下:

sAuYluklo9.png!large

进度条

对于长时间执行的任务,我们可以通过加一个进度条来实时展现处理进度,这样看上去会更直观。处理流程如下:

$this->line('开始处理任务...');
$timers = [1, 2, 3, 4, 5]; // 定义遍历数组

$bar = $this->output->createProgressBar(count($timers)); // 创建进度条,并初始化步数
$bar->start(); // 启动进度条

// 遍历数组
foreach ($timers as $timer) {
    sleep($timer);   // 程序处理逻辑
    $bar->advance(); // 推进进度条
}

$bar->finish(); // 结束进度条
$this->newLine();
$this->info('完成任务!');

其显示效果如下:

rDflsJWei9.gif!large

当我们需要处理比较耗时且需要按步骤执行的操作时,加上这么一个进度条,是不是就很方便了呢。

至此,我们已经了解了命令类的基本结构、输入及输出的逻辑,剩下的就是处理我们实际业务中的逻辑了。

这里我们再来介绍一下实际应用中比较方便的一种命令实现方式。

实际应用

我们可以发现,上面例子中的应用场景都是一个命令对应一个类文件,这样设计命令类的处理逻辑比较单一,但当我们的命令越来越多的时候,会在命令目录下生成一堆的「命令文件」,不方便维护。

这里我们可以把命令类稍微改造一下,使命令类作为一个统一的入口,我们要执行的脚本作为参数传递,这样就可以通过传递不同的参数执行不同的脚本了。示例代码如下:

class MissionScript extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'script:mission
                            {name : 任务名称}
                            {--p|parameter=? : 任务参数}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '任务脚本';

    /**
     * 任务配置
     *
     * @var array
     */
    protected $missions = [
        'sendMail' => [
            'desc' => '发送邮件任务',
            'callback' => 'sendMail',
        ],
    ];

   /**
    * 任务服务
    * 
    * @var MissionService 
    */
    protected $missionService;

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct(MissionService $missionService)
    {
        parent::__construct();
        $this->missionService = $missionService;
    }

    /**
     * Execute the console command.
     *
     * @return int
     * @throws \Exception
     */
    public function handle()
    {
        $missionName = $this->argument('name');
        $parameter = $this->option('parameter');

        if(!isset($this->missions[$missionName])){
            throw new \Exception("[{$missionName}]任务没有配置,请先进行配置");
        }

        $callback = Arr::get($this->missions, "{$missionName}.callback");
        if(!$callback){
            throw new \Exception("[{$missionName}]任务的回调方法没有配置");
        }

        if (method_exists($this, $callback)) {
            $handler = $this;
        } else if (is_callable([$this->missionService, $callback])) {
            $handler = $this->missionService;
        } else {
            throw new \Exception("[{$missionName}]任务的回调方法[{$callback}]不可调用");
        }

        $data = json_decode($parameter, true);
        $data = $data ? $data : [];

        return call_user_func_array([$handler, $callback], [$data]);
    }

    /**
     * 发送邮件
     *
     * @param array $data 参数
     */
    public function sendMail(array $data = [])
    {
        $users = $data['users'] ?? [];
        $sendAt = $data['sendAt'] ?? '';

        $this->info('`sendMail` 任务脚本将于 [' . $sendAt . '] 向 [' . implode(',', $users) . '] 发送邮件');
    }
}

关于这个任务类,有以下几点需要说明一下:

  • 命令接收一个必选的 {命令名称} 参数 和一个可选的 {命令参数}{命令参数} 格式为 json,扩展性更强。
  • 增加一个 missions 属性相当于维护一套任务配置,只有正确配置的脚本才可以被调用,配置还有一个作用就是可以帮助接手项目的人快速了解脚本中有哪些可用的任务。
  • 增加一个 MissionService 服务是为了把部分不需要交互场景的任务放在 MissionService 中维护(比如需要在后台运行的定时任务),而且 MissionService 中的任务还可以作为服务被其他调用方调用,提高了复用性,而那些需要交互场景的任务则直接放在命令类中维护。
  • 调用任务的时候,会优先在 MissionService 中查找,如果存在的话则直接调用,没有的话则在当前任务类中进行查找。

这样我们就可以通过以下方式调用命令了:

php artisan script:mission sendMail -p '{"users":["Tom", "Sam"],"sendAt":"2023-05-20 15:00"}'

命令的运行结果如下:

`sendMail` 任务脚本将于 [2023-05-20 15:00][Tom,Sam] 发送邮件

当这个「任务脚本」变的越来越庞大的时候,我们还可以考虑进行更进一步的归类,从而达到既方便维护,又方便开发的目的。

结语

到这里,关于 Artisan 入门的知识我们就介绍完了。关于 Artisan 其他方面的一些使用小技巧后续我们会通过具体的话题进行讨论,感谢大家持续关注~

本作品采用《CC 协议》,转载必须注明作者和本文链接
你应该了解真相,真相会让你自由。
本帖由系统于 10个月前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 10

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!