diff --git a/UPGRADE-6.1.md b/UPGRADE-6.1.md index d48aca725b3f..825b2109f0c6 100644 --- a/UPGRADE-6.1.md +++ b/UPGRADE-6.1.md @@ -12,6 +12,8 @@ Console ------- * Deprecate `Command::$defaultName` and `Command::$defaultDescription`, use the `AsCommand` attribute instead + * Add argument `$suggestedValues` to `Command::addArgument` and `Command::addOption` + * Add argument `$suggestedValues` to `InputArgument` and `InputOption` constructors HttpKernel ---------- diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 2573354f04c2..22c404363aa5 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add method `__toString()` to `InputInterface` * Deprecate `Command::$defaultName` and `Command::$defaultDescription`, use the `AsCommand` attribute instead + * Add suggested values for arguments and options in input definition, for input completion 6.0 --- diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index 147682e1e954..0a3f4b7889e5 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -15,6 +15,7 @@ use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; @@ -319,6 +320,12 @@ public function run(InputInterface $input, OutputInterface $output): int */ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { + $definition = $this->getDefinition(); + if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() && $definition->hasOption($input->getCompletionName())) { + $definition->getOption($input->getCompletionName())->complete($input, $suggestions); + } elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && $definition->hasArgument($input->getCompletionName())) { + $definition->getArgument($input->getCompletionName())->complete($input, $suggestions); + } } /** @@ -427,17 +434,22 @@ public function getNativeDefinition(): InputDefinition /** * Adds an argument. * - * @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL - * @param mixed $default The default value (for InputArgument::OPTIONAL mode only) + * @param $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL + * @param $default The default value (for InputArgument::OPTIONAL mode only) + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion * * @throws InvalidArgumentException When argument mode is not valid * * @return $this */ - public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null): static + public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null, /*array|\Closure $suggestedValues = null*/): static { - $this->definition->addArgument(new InputArgument($name, $mode, $description, $default)); - $this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default)); + $suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : []; + if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) { + throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues))); + } + $this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues)); + $this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues)); return $this; } @@ -448,15 +460,20 @@ public function addArgument(string $name, int $mode = null, string $description * @param $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts * @param $mode The option mode: One of the InputOption::VALUE_* constants * @param $default The default value (must be null for InputOption::VALUE_NONE) + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion * * @throws InvalidArgumentException If option mode is invalid or incompatible * * @return $this */ - public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null): static + public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null, /*array|\Closure $suggestedValues = []*/): static { - $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); - $this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); + $suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : []; + if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) { + throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues))); + } + $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues)); + $this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues)); return $this; } diff --git a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php index d70c18374178..7597a9e2cc65 100644 --- a/src/Symfony/Component/Console/Command/DumpCompletionCommand.php +++ b/src/Symfony/Component/Console/Command/DumpCompletionCommand.php @@ -12,8 +12,6 @@ namespace Symfony\Component\Console\Command; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -39,12 +37,7 @@ final class DumpCompletionCommand extends Command */ protected static $defaultDescription = 'Dump the shell completion script'; - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestArgumentValuesFor('shell')) { - $suggestions->suggestValues($this->getSupportedShells()); - } - } + private array $supportedShells; protected function configure() { @@ -82,7 +75,7 @@ protected function configure() eval "$(${fullCommand} completion bash)" EOH ) - ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given') + ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given', null, $this->getSupportedShells(...)) ->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log') ; } @@ -135,7 +128,7 @@ private function tailDebugLog(string $commandName, OutputInterface $output): voi */ private function getSupportedShells(): array { - return array_map(function ($f) { + return $this->supportedShells ??= array_map(function ($f) { return pathinfo($f, \PATHINFO_EXTENSION); }, glob(__DIR__.'/../Resources/completion.*')); } diff --git a/src/Symfony/Component/Console/Command/HelpCommand.php b/src/Symfony/Component/Console/Command/HelpCommand.php index ad4b813e44e4..f5ddd5204fae 100644 --- a/src/Symfony/Component/Console/Command/HelpCommand.php +++ b/src/Symfony/Component/Console/Command/HelpCommand.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Console\Command; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Descriptor\ApplicationDescription; use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; @@ -39,8 +37,12 @@ protected function configure() $this ->setName('help') ->setDefinition([ - new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help'), - new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), + new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help', function () { + return array_keys((new ApplicationDescription($this->getApplication()))->getCommands()); + }), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', function () { + return (new DescriptorHelper())->getFormats(); + }), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), ]) ->setDescription('Display help for a command') @@ -81,19 +83,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestArgumentValuesFor('command_name')) { - $descriptor = new ApplicationDescription($this->getApplication()); - $suggestions->suggestValues(array_keys($descriptor->getCommands())); - - return; - } - - if ($input->mustSuggestOptionValuesFor('format')) { - $helper = new DescriptorHelper(); - $suggestions->suggestValues($helper->getFormats()); - } - } } diff --git a/src/Symfony/Component/Console/Command/LazyCommand.php b/src/Symfony/Component/Console/Command/LazyCommand.php index b674a6387b32..cb4d7f245195 100644 --- a/src/Symfony/Component/Console/Command/LazyCommand.php +++ b/src/Symfony/Component/Console/Command/LazyCommand.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Application; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; @@ -108,16 +109,28 @@ public function getNativeDefinition(): InputDefinition return $this->getCommand()->getNativeDefinition(); } - public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null): static + /** + * {@inheritdoc} + * + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion + */ + public function addArgument(string $name, int $mode = null, string $description = '', mixed $default = null, /*array|\Closure $suggestedValues = []*/): static { - $this->getCommand()->addArgument($name, $mode, $description, $default); + $suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : []; + $this->getCommand()->addArgument($name, $mode, $description, $default, $suggestedValues); return $this; } - public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null): static + /** + * {@inheritdoc} + * + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion + */ + public function addOption(string $name, string|array $shortcut = null, int $mode = null, string $description = '', mixed $default = null, /*array|\Closure $suggestedValues = []*/): static { - $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default); + $suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : []; + $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues); return $this; } diff --git a/src/Symfony/Component/Console/Command/ListCommand.php b/src/Symfony/Component/Console/Command/ListCommand.php index 5c7260fe4c07..420bdde28004 100644 --- a/src/Symfony/Component/Console/Command/ListCommand.php +++ b/src/Symfony/Component/Console/Command/ListCommand.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Console\Command; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Descriptor\ApplicationDescription; use Symfony\Component\Console\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; @@ -35,9 +33,13 @@ protected function configure() $this ->setName('list') ->setDefinition([ - new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, function () { + return array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces()); + }), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), - new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', function () { + return (new DescriptorHelper())->getFormats(); + }), new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'), ]) ->setDescription('List commands') @@ -77,19 +79,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestArgumentValuesFor('namespace')) { - $descriptor = new ApplicationDescription($this->getApplication()); - $suggestions->suggestValues(array_keys($descriptor->getNamespaces())); - - return; - } - - if ($input->mustSuggestOptionValuesFor('format')) { - $helper = new DescriptorHelper(); - $suggestions->suggestValues($helper->getFormats()); - } - } } diff --git a/src/Symfony/Component/Console/Input/InputArgument.php b/src/Symfony/Component/Console/Input/InputArgument.php index 143e4b10ae71..f87bf404e3ec 100644 --- a/src/Symfony/Component/Console/Input/InputArgument.php +++ b/src/Symfony/Component/Console/Input/InputArgument.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Console\Input; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; @@ -28,6 +32,7 @@ class InputArgument private string $name; private int $mode; private string|int|bool|array|null|float $default; + private array|\Closure $suggestedValues; private string $description; /** @@ -35,10 +40,11 @@ class InputArgument * @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL * @param string $description A description text * @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only) + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion * * @throws InvalidArgumentException When argument mode is not valid */ - public function __construct(string $name, int $mode = null, string $description = '', string|bool|int|float|array $default = null) + public function __construct(string $name, int $mode = null, string $description = '', string|bool|int|float|array $default = null, \Closure|array $suggestedValues = []) { if (null === $mode) { $mode = self::OPTIONAL; @@ -49,6 +55,7 @@ public function __construct(string $name, int $mode = null, string $description $this->name = $name; $this->mode = $mode; $this->description = $description; + $this->suggestedValues = $suggestedValues; $this->setDefault($default); } @@ -111,6 +118,27 @@ public function getDefault(): string|bool|int|float|array|null return $this->default; } + public function hasCompletion(): bool + { + return [] !== $this->suggestedValues; + } + + /** + * Adds suggestions to $suggestions for the current completion input. + * + * @see Command::complete() + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $values = $this->suggestedValues; + if ($values instanceof \Closure && !\is_array($values = $values($input))) { + throw new LogicException(sprintf('Closure for argument "%s" must return an array. Got "%s".', $this->name, get_debug_type($values))); + } + if ($values) { + $suggestions->suggestValues($values); + } + } + /** * Returns the description text. */ diff --git a/src/Symfony/Component/Console/Input/InputOption.php b/src/Symfony/Component/Console/Input/InputOption.php index f9d74a8961ce..5ea4a7f47e7e 100644 --- a/src/Symfony/Component/Console/Input/InputOption.php +++ b/src/Symfony/Component/Console/Input/InputOption.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Console\Input; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; @@ -50,16 +54,18 @@ class InputOption private string|array|null $shortcut; private int $mode; private string|int|bool|array|null|float $default; + private array|\Closure $suggestedValues; private string $description; /** * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts * @param int|null $mode The option mode: One of the VALUE_* constants * @param string|bool|int|float|array|null $default The default value (must be null for self::VALUE_NONE) + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion * * @throws InvalidArgumentException If option mode is invalid or incompatible */ - public function __construct(string $name, string|array $shortcut = null, int $mode = null, string $description = '', string|bool|int|float|array $default = null) + public function __construct(string $name, string|array $shortcut = null, int $mode = null, string $description = '', string|bool|int|float|array $default = null, array|\Closure $suggestedValues = []) { if (str_starts_with($name, '--')) { $name = substr($name, 2); @@ -96,7 +102,11 @@ public function __construct(string $name, string|array $shortcut = null, int $mo $this->shortcut = $shortcut; $this->mode = $mode; $this->description = $description; + $this->suggestedValues = $suggestedValues; + if ($suggestedValues && !$this->acceptValue()) { + throw new LogicException('Cannot set suggested values if the option does not accept a value.'); + } if ($this->isArray() && !$this->acceptValue()) { throw new InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); } @@ -201,6 +211,27 @@ public function getDescription(): string return $this->description; } + public function hasCompletion(): bool + { + return [] !== $this->suggestedValues; + } + + /** + * Adds suggestions to $suggestions for the current completion input. + * + * @see Command::complete() + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $values = $this->suggestedValues; + if ($values instanceof \Closure && !\is_array($values = $values($input))) { + throw new LogicException(sprintf('Closure for option "%s" must return an array. Got "%s".', $this->name, get_debug_type($values))); + } + if ($values) { + $suggestions->suggestValues($values); + } + } + /** * Checks whether the given option equals this one. */ diff --git a/src/Symfony/Component/Console/Tests/Command/CommandTest.php b/src/Symfony/Component/Console/Tests/Command/CommandTest.php index 1d0a383de7d8..6aca4e4383b0 100644 --- a/src/Symfony/Component/Console/Tests/Command/CommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/CommandTest.php @@ -88,6 +88,16 @@ public function testAddArgument() $this->assertTrue($command->getDefinition()->hasArgument('foo'), '->addArgument() adds an argument to the command'); } + public function testAddArgumentFull() + { + $command = new \TestCommand(); + $command->addArgument('foo', InputArgument::OPTIONAL, 'Description', 'default', ['a', 'b']); + $argument = $command->getDefinition()->getArgument('foo'); + $this->assertSame('Description', $argument->getDescription()); + $this->assertSame('default', $argument->getDefault()); + $this->assertTrue($argument->hasCompletion()); + } + public function testAddOption() { $command = new \TestCommand(); @@ -96,6 +106,17 @@ public function testAddOption() $this->assertTrue($command->getDefinition()->hasOption('foo'), '->addOption() adds an option to the command'); } + public function testAddOptionFull() + { + $command = new \TestCommand(); + $command->addOption('foo', ['f'], InputOption::VALUE_OPTIONAL, 'Description', 'default', ['a', 'b']); + $option = $command->getDefinition()->getOption('foo'); + $this->assertSame('f', $option->getShortcut()); + $this->assertSame('Description', $option->getDescription()); + $this->assertSame('default', $option->getDefault()); + $this->assertTrue($option->hasCompletion()); + } + public function testSetHidden() { $command = new \TestCommand(); diff --git a/src/Symfony/Component/Console/Tests/Input/InputArgumentTest.php b/src/Symfony/Component/Console/Tests/Input/InputArgumentTest.php index 39a3de5c6200..814cfe388c12 100644 --- a/src/Symfony/Component/Console/Tests/Input/InputArgumentTest.php +++ b/src/Symfony/Component/Console/Tests/Input/InputArgumentTest.php @@ -12,6 +12,10 @@ namespace Symfony\Component\Console\Tests\Input; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Input\InputArgument; class InputArgumentTest extends TestCase @@ -95,4 +99,33 @@ public function testSetDefaultWithArrayArgument() $argument = new InputArgument('foo', InputArgument::IS_ARRAY); $argument->setDefault('default'); } + + public function testCompleteArray() + { + $values = ['foo', 'bar']; + $argument = new InputArgument('foo', null, '', null, $values); + $this->assertTrue($argument->hasCompletion()); + $suggestions = new CompletionSuggestions(); + $argument->complete(new CompletionInput(), $suggestions); + $this->assertSame($values, array_map(fn (Suggestion $suggestion) => $suggestion->getValue(), $suggestions->getValueSuggestions())); + } + + public function testCompleteClosure() + { + $values = ['foo', 'bar']; + $argument = new InputArgument('foo', null, '', null, fn (CompletionInput $input): array => $values); + $this->assertTrue($argument->hasCompletion()); + $suggestions = new CompletionSuggestions(); + $argument->complete(new CompletionInput(), $suggestions); + $this->assertSame($values, array_map(fn (Suggestion $suggestion) => $suggestion->getValue(), $suggestions->getValueSuggestions())); + } + + public function testCompleteClosureReturnIncorrectType() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Closure for argument "foo" must return an array. Got "string".'); + + $argument = new InputArgument('foo', InputArgument::OPTIONAL, '', null, fn (CompletionInput $input) => 'invalid'); + $argument->complete(new CompletionInput(), new CompletionSuggestions()); + } } diff --git a/src/Symfony/Component/Console/Tests/Input/InputOptionTest.php b/src/Symfony/Component/Console/Tests/Input/InputOptionTest.php index 943bcf628c58..0b5271b324ae 100644 --- a/src/Symfony/Component/Console/Tests/Input/InputOptionTest.php +++ b/src/Symfony/Component/Console/Tests/Input/InputOptionTest.php @@ -12,6 +12,10 @@ namespace Symfony\Component\Console\Tests\Input; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Input\InputOption; class InputOptionTest extends TestCase @@ -194,4 +198,41 @@ public function testEquals() $option2 = new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL, 'Some description'); $this->assertFalse($option->equals($option2)); } + + public function testSuggestedValuesErrorIfNoValue() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Cannot set suggested values if the option does not accept a value.'); + + new InputOption('foo', null, InputOption::VALUE_NONE, '', null, ['foo']); + } + + public function testCompleteArray() + { + $values = ['foo', 'bar']; + $option = new InputOption('foo', null, InputOption::VALUE_OPTIONAL, '', null, $values); + $this->assertTrue($option->hasCompletion()); + $suggestions = new CompletionSuggestions(); + $option->complete(new CompletionInput(), $suggestions); + $this->assertSame($values, array_map(fn (Suggestion $suggestion) => $suggestion->getValue(), $suggestions->getValueSuggestions())); + } + + public function testCompleteClosure() + { + $values = ['foo', 'bar']; + $option = new InputOption('foo', null, InputOption::VALUE_OPTIONAL, '', null, fn (CompletionInput $input): array => $values); + $this->assertTrue($option->hasCompletion()); + $suggestions = new CompletionSuggestions(); + $option->complete(new CompletionInput(), $suggestions); + $this->assertSame($values, array_map(fn (Suggestion $suggestion) => $suggestion->getValue(), $suggestions->getValueSuggestions())); + } + + public function testCompleteClosureReturnIncorrectType() + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Closure for option "foo" must return an array. Got "string".'); + + $option = new InputOption('foo', null, InputOption::VALUE_OPTIONAL, '', null, fn (CompletionInput $input) => 'invalid'); + $option->complete(new CompletionInput(), new CompletionSuggestions()); + } }