使用必须指定值的选项创建 Artisan 命令

Create Artisan command with option that must specify a value

Laravel's documentation 说(强调我的):

If the user must specify a value for an option, you should suffix the option name with a = sign…

但接着说:

If the option is not specified when invoking the command, its value will be null

这表明“必须”并不代表我认为的意思。确实如此。带有如下签名的简单命令:

protected $signature = "mycommand {-t|test=}";

运行 像 artisan mycommand -t 这样调用时会很好。更糟糕的是,如果您指定了默认值,则在这种情况下不会应用。

protected $signature = "mycommand {-t|test=42}";

当运行宁artisan mycommand时,$this->option('test')会给你一个值42,但是当运行作为artisan mycommand -t它给你一个值null.

那么,有没有办法要求用户必须(实际上)为给定选项指定一个值(如果它出现在命令行上)?

the Laravel code, I confirmed that there is no way to have a truly "required" value. Although Symfony does provide 中查找所需的值,Laravel 不使用此功能。相反,选项都是作为可选的创建的,所以我将不得不编写自己的解析器...

这很简单;我必须编写自定义解析器 class 来覆盖 Illuminate\Console\Parser::parseOption() 方法,然后覆盖 Illuminate\Console\Command::configureUsingFluentDefinition() 以使用新的 class.

我选择创建一个新的选项类型,而不是更改任何现有命令选项的行为。所以现在当我想强制一个值时,我这样声明我的签名:

<?php

namespace App\Console\Commands;

use App\Console\Command;

class MyCommand extends Command
{
    /** @var string The double == means a required value */
    protected $signature = "mycommand {--t|test==}";

    ...
}

尝试 运行 artisan mycommand -t 现在将抛出 Symfony\Component\Console\Exception\RuntimeException 消息“--test 选项需要一个值。 " 这也适用于具有默认值的数组选项 (--t==*) and/or 选项 (--t==42--t==*42.)

这是新解析器的代码 class:

<?php

namespace App\Console;

use Illuminate\Console\Parser as BaseParser;
use Symfony\Component\Console\Input\InputOption;

class Parser extends BaseParser
{
    protected static function parseOption($token): InputOption
    {
        [$mytoken, $description] = static::extractDescription($token);

        $matches = preg_split("/\s*\|\s*/", $mytoken, 2);

        if (isset($matches[1])) {
            $shortcut = $matches[0];
            $mytoken = $matches[1];
        } else {
            $shortcut = null;
        }

        switch (true) {
            case str_ends_with($mytoken, "=="):
                return new InputOption(
                    trim($mytoken, "="),
                    $shortcut,
                    InputOption::VALUE_REQUIRED,
                    $description
                );
            case str_ends_with($mytoken, "==*"):
                return new InputOption(
                    trim($mytoken, "=*"),
                    $shortcut,
                    InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
                    $description
                );
            case preg_match("/(.+)==\*(.+)/", $mytoken, $matches):
                return new InputOption(
                    $matches[1],
                    $shortcut,
                    InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
                    $description,
                    preg_split('/,\s?/', $matches[2])
                );
            case preg_match("/(.+)==(.+)/", $mytoken, $matches):
                return new InputOption(
                    $matches[1],
                    $shortcut,
                    InputOption::VALUE_REQUIRED,
                    $description,
                    $matches[2]
                );
            default:
                // no == here, fall back to the standard parser
                return parent::parseOption($token);
        }
    }
}

和新命令class:

<?php

namespace App\Console;

use Illuminate\Console\Command as BaseCommand;

class Command extends BaseCommand
{
    /**
     * Overriding the Laravel parser so we can have required arguments
     *
     * @inheritdoc
     * @throws ReflectionException
     */
    protected function configureUsingFluentDefinition(): void
    {
        // using our parser here
        [$name, $arguments, $options] = Parser::parse($this->signature);

        // need to call the great-grandparent constructor here; probably
        // could have hard-coded to Symfony, but better safe than sorry
        $reflectionMethod = new ReflectionMethod(
            get_parent_class(BaseCommand::class),
            "__construct"
        );
        $reflectionMethod->invoke($this, $name);

        $this->getDefinition()->addArguments($arguments);
        $this->getDefinition()->addOptions($options);
    }
}