'Create Artisan command with option that must specify a value
Laravel's documentation says (emphasis mine):
If the user must specify a value for an option, you should suffix the option name with a
=
sign…
But then goes on to say:
If the option is not specified when invoking the command, its value will be
null
…
Which suggests that "must" doesn't mean what I think it means. And indeed that is the case. A simple command with a signature like this:
protected $signature = "mycommand {-t|test=}";
Will run just fine when called like artisan mycommand -t
. And what's worse is that if you specify a default value, it isn't applied in this case.
protected $signature = "mycommand {-t|test=42}";
When running artisan mycommand
, $this->option('test')
will give you a value of 42, but when run as artisan mycommand -t
it gives a value of null
.
So, is there a way to require that a user must (actually) specify a value for a given option, if it's present on the command line?
Solution 1:[1]
Poking around the Laravel code, I confirmed that there is no way to have a truly "required" value. Although Symfony does provide for required values, Laravel doesn't use this capability. Instead the options are all created as optional, so I will have to write my own parser...
This was fairly straightforward; I had to write a custom parser class to override the Illuminate\Console\Parser::parseOption()
method, and then override Illuminate\Console\Command::configureUsingFluentDefinition()
to use that new class.
I elected to create a new option type, rather than change the behaviour of any existing command options. So now I declare my signature like this when I want to force a value:
<?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==}";
...
}
Attempting to run artisan mycommand -t
will now throw a Symfony\Component\Console\Exception\RuntimeException
with a message of "The --test option requires a value." This also works for array options (--t==*
) and/or options with default values (--t==42
or --t==*42
.)
Here's the code for the new parser 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);
}
}
}
And the new command 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);
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |