Skip to content

Commit 373bb7d

Browse files
committed
[Console] Command: Add addOptionWithValue()
1 parent e06b9ac commit 373bb7d

File tree

8 files changed

+95
-7
lines changed

8 files changed

+95
-7
lines changed

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Add `Command::getCode()` to get the code set via `setCode()`
8+
* Add `Command::addOptionWithValue()` to add option with allowed values
89
* Allow setting aliases and the hidden flag via the command name passed to the constructor
910
* Introduce `Symfony\Component\Console\Application::addCommand()` to simplify using invokable commands when the component is used standalone
1011
* Deprecate `Symfony\Component\Console\Application::add()` in favor of `Symfony\Component\Console\Application::addCommand()`

src/Symfony/Component/Console/Command/Command.php

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,34 @@ public function addArgument(string $name, ?int $mode = null, string $description
480480
*/
481481
public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
482482
{
483-
$this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
484-
$this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
483+
return $this->addOptionDefinition(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
484+
}
485+
486+
/**
487+
* @param non-empty-array<string|int, string> $valueDescriptions Mapping from allowed values to their descriptions
488+
*
489+
* @return $this
490+
*
491+
* @throws InvalidArgumentException If option mode is invalid or incompatible
492+
*/
493+
public function addOptionWithValue(string $name, array $valueDescriptions, string|array|null $shortcut = null, string $description = ''): static
494+
{
495+
$suggestedValues = [];
496+
foreach ($valueDescriptions as $value => $valueDescription) {
497+
$suggestedValues[] = new Suggestion((string) $value, $valueDescription);
498+
$description .= \sprintf('%s<info>%s</info>: %s', '' !== $description ? \PHP_EOL : '', $value, $valueDescription);
499+
}
500+
501+
$option = new InputOption($name, $shortcut, InputOption::VALUE_REQUIRED, $description, null, $suggestedValues);
502+
$option->setAllowedValues(array_keys($valueDescriptions));
503+
504+
return $this->addOptionDefinition($option);
505+
}
506+
507+
private function addOptionDefinition(InputOption $inputOption): static
508+
{
509+
$this->definition->addOption($inputOption);
510+
$this->fullDefinition?->addOption($inputOption);
485511

486512
return $this;
487513
}

src/Symfony/Component/Console/Input/ArgvInput.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ private function addLongOption(string $name, mixed $value): void
265265
if ($option->isArray()) {
266266
$this->options[$name][] = $value;
267267
} else {
268-
$this->options[$name] = $value;
268+
$this->setOption($name, $value);
269269
}
270270
}
271271

src/Symfony/Component/Console/Input/ArrayInput.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ private function addShortOption(string $shortcut, mixed $value): void
145145
* Adds a long option value.
146146
*
147147
* @throws InvalidOptionException When option given doesn't exist
148-
* @throws InvalidOptionException When a required value is missing
148+
* @throws InvalidOptionException When a required value is missing or invalid
149149
*/
150150
private function addLongOption(string $name, mixed $value): void
151151
{
@@ -172,7 +172,7 @@ private function addLongOption(string $name, mixed $value): void
172172
}
173173
}
174174

175-
$this->options[$name] = $value;
175+
$this->setOption($name, $value);
176176
}
177177

178178
/**

src/Symfony/Component/Console/Input/Input.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Console\Input;
1313

1414
use Symfony\Component\Console\Exception\InvalidArgumentException;
15+
use Symfony\Component\Console\Exception\InvalidOptionException;
1516
use Symfony\Component\Console\Exception\RuntimeException;
1617

1718
/**
@@ -140,6 +141,12 @@ public function setOption(string $name, mixed $value): void
140141
throw new InvalidArgumentException(\sprintf('The "%s" option does not exist.', $name));
141142
}
142143

144+
$option = $this->definition->getOption($name);
145+
$allowedValues = $option->getAllowedValues();
146+
if (null !== $allowedValues && !\in_array($value, $allowedValues, true)) {
147+
throw InvalidOptionException::fromEnumValue($name, $value, $allowedValues);
148+
}
149+
143150
$this->options[$name] = $value;
144151
}
145152

src/Symfony/Component/Console/Input/InputOption.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ class InputOption
5555
private int $mode;
5656
private string|int|bool|array|float|null $default;
5757

58+
/** @var ?non-empty-list<string|int> */
59+
private ?array $allowedValues = null;
60+
5861
/**
5962
* @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts
6063
* @param int-mask-of<InputOption::*>|null $mode The option mode: One of the VALUE_* constants
@@ -229,6 +232,26 @@ public function hasCompletion(): bool
229232
return [] !== $this->suggestedValues;
230233
}
231234

235+
/**
236+
* @param non-empty-list<string|int> $allowedValues
237+
*
238+
* @return $this
239+
*/
240+
public function setAllowedValues(array $allowedValues): static
241+
{
242+
$this->allowedValues = $allowedValues;
243+
244+
return $this;
245+
}
246+
247+
/**
248+
* @return ?non-empty-list<string>
249+
*/
250+
public function getAllowedValues(): ?array
251+
{
252+
return $this->allowedValues;
253+
}
254+
232255
/**
233256
* Supplies suggestions when command resolves possible completion options for input.
234257
*
@@ -257,6 +280,7 @@ public function equals(self $option): bool
257280
&& $option->isArray() === $this->isArray()
258281
&& $option->isValueRequired() === $this->isValueRequired()
259282
&& $option->isValueOptional() === $this->isValueOptional()
283+
&& $option->getAllowedValues() === $this->getAllowedValues()
260284
;
261285
}
262286
}

src/Symfony/Component/Console/Tests/Command/CommandTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,23 @@ public function testAddOptionFull()
120120
$this->assertTrue($option->hasCompletion());
121121
}
122122

123+
public function testAddOptionWithValue()
124+
{
125+
$command = new \TestCommand();
126+
$ret = $command->addOptionWithValue('foo', ['a' => 'Do A', 'b' => 'Do B'], null, 'Foo');
127+
$this->assertEquals($command, $ret, '->addOptionWithValue() implements a fluent interface');
128+
$this->assertTrue($command->getDefinition()->hasOption('foo'), '->addOptionWithValue() adds an option to the command');
129+
$option = $command->getDefinition()->getOption('foo');
130+
$this->assertEquals(['a', 'b'], $option->getAllowedValues(), '->addOptionWithValue() sets allowed values');
131+
132+
$this->assertEquals('Foo'.\PHP_EOL.'<info>a</info>: Do A'.\PHP_EOL.'<info>b</info>: Do B', $option->getDescription());
133+
134+
$tester = new CommandTester($command);
135+
$this->expectException(InvalidOptionException::class);
136+
$this->expectExceptionMessage('The value "invalid" is not valid for the "foo" option. Supported values are "a", "b".');
137+
$tester->execute(['--foo' => 'invalid']);
138+
}
139+
123140
public function testSetHidden()
124141
{
125142
$command = new \TestCommand();

src/Symfony/Component/Console/Tests/Input/ArgvInputTest.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\Attributes\DataProvider;
1515
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\Console\Exception\InvalidOptionException;
1617
use Symfony\Component\Console\Input\ArgvInput;
1718
use Symfony\Component\Console\Input\InputArgument;
1819
use Symfony\Component\Console\Input\InputDefinition;
@@ -108,6 +109,12 @@ public static function provideOptions()
108109
['foo' => null],
109110
'->parse() parses long options with optional value specified with no separator and no value as null',
110111
],
112+
[
113+
['cli.php', '--foo=a'],
114+
[(new InputOption('foo', 'f', InputOption::VALUE_REQUIRED))->setAllowedValues(['a', 'b'])],
115+
['foo' => 'a'],
116+
'->parse() parses long options with allowed values',
117+
],
111118
[
112119
['cli.php', '-f'],
113120
[new InputOption('foo', 'f')],
@@ -232,9 +239,9 @@ public static function provideNegatableOptions()
232239
}
233240

234241
#[DataProvider('provideInvalidInput')]
235-
public function testInvalidInput($argv, $definition, $expectedExceptionMessage)
242+
public function testInvalidInput($argv, $definition, $expectedExceptionMessage, $exceptionClass = \RuntimeException::class)
236243
{
237-
$this->expectException(\RuntimeException::class);
244+
$this->expectException($exceptionClass);
238245
$this->expectExceptionMessage($expectedExceptionMessage);
239246

240247
(new ArgvInput($argv))->bind($definition);
@@ -272,6 +279,12 @@ public static function provideInvalidInput(): array
272279
new InputDefinition([new InputOption('foo', 'f', InputOption::VALUE_NONE)]),
273280
'The "--foo" option does not accept a value.',
274281
],
282+
[
283+
['cli.php', '--foo=invalid'],
284+
new InputDefinition([(new InputOption('foo', 'f', InputOption::VALUE_REQUIRED))->setAllowedValues(['a', 'b'])]),
285+
'The value "invalid" is not valid for the "foo" option. Supported values are "a", "b".',
286+
InvalidOptionException::class,
287+
],
275288
[
276289
['cli.php', 'foo', 'bar'],
277290
new InputDefinition(),

0 commit comments

Comments
 (0)