8000 [Console] Invokable command `#[Option]` adjustments · symfony/symfony@59a4ae9 · GitHub
[go: up one dir, main page]

Skip to content

Commit 59a4ae9

Browse files
committed
[Console] Invokable command #[Option] adjustments
- `#[Option] ?string $opt = null` as `VALUE_REQUIRED` - `#[Option] bool|string $opt = false` as `VALUE_OPTIONAL` - `#[Option] ?string $opt = ''` throws exception - allow `#[Option] ?array $opt = null` - more tests...
1 parent db8e84d commit 59a4ae9

File tree

2 files changed

+118
-35
lines changed

2 files changed

+118
-35
lines changed

src/Symfony/Component/Console/Attribute/Option.php

Lines changed: 53 additions & 21 deletions
E29B
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
class Option
2323
{
2424
private const ALLOWED_TYPES = ['string', 'bool', 'int', 'float', 'array'];
25+
private const ALLOWED_UNION_TYPES = ['bool|string', 'bool|int', 'bool|float'];
2526

2627
private string|bool|int|float|array|null $default = null;
2728
private array|\Closure $suggestedValues;
@@ -56,18 +57,8 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self
5657
return null;
5758
}
5859

59-
$type = $parameter->getType();
6060
$name = $parameter->getName();
61-
62-
if (!$type instanceof \ReflectionNamedType) {
63-
throw new LogicException(\sprintf('The parameter "$%s" must have a named type. Untyped, Union or Intersection types are not supported for command options.', $name));
64-
}
65-
66-
$self->typeName = $type->getName();
67-
68-
if (!\in_array($self->typeName, self::ALLOWED_TYPES, true)) {
69-
throw new LogicException(\sprintf('The type "%s" of parameter "$%s" is not supported as a command option. Only "%s" types are allowed.', $self->typeName, $name, implode('", "', self::ALLOWED_TYPES)));
70-
}
61+
$type = $parameter->getType();
7162

7263
if (!$parameter->isDefaultValueAvailable()) {
7364
throw new LogicException(\sprintf('The option parameter "$%s" must declare a default value.', $name));
@@ -80,28 +71,37 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self
8071
$self->default = $parameter->getDefaultValue();
8172
$self->allowNull = $parameter->allowsNull();
8273

83-
if ('bool' === $self->typeName && $self->allowNull && \in_array($self->default, [true, false], true)) {
84-
throw new LogicException(\sprintf('The option parameter "$%s" must not be nullable when it has a default boolean value.', $name));
74+
if ($type instanceof \ReflectionUnionType) {
75+
return $self->handleUnion($type);
8576
}
8677

87-
if ('string' === $self->typeName && null === $self->default) {
88-
throw new LogicException(\sprintf('The option parameter "$%s" must not have a default of null.', $name));
78+
if (!$type instanceof \ReflectionNamedType) {
79+
throw new LogicException(\sprintf('The parameter "$%s" must have a named type. Untyped or Intersection types are not supported for command options.', $name));
8980
}
9081

91-
if ('array' === $self->typeName && $self->allowNull) {
92-
throw new LogicException(\sprintf('The option parameter "$%s" must not be nullable.', $name));
82+
$self->typeName = $type->getName();
83+
84+
if (!\in_array($self->typeName, self::ALLOWED_TYPES, true)) {
85+
throw new LogicException(\sprintf('The type "%s" of parameter "$%s" is not supported as a command option. Only "%s" types are allowed.', $self->typeName, $name, implode('", "', self::ALLOWED_TYPES)));
86+
}
87+
88+
if ('bool' === $self->typeName && $self->allowNull && \in_array($self->default, [true, false], true)) {
89+
throw new LogicException(\sprintf('The option parameter "$%s" must not be nullable when it has a default boolean value.', $name));
90+
}
91+
92+
if ($self->allowNull && null !== $self->default) {
93+
throw new LogicException(\sprintf('The option parameter "$%s" must either be not-nullable or have a default of null.', $name));
9394
}
9495

9596
if ('bool' === $self->typeName) {
9697
$self->mode = InputOption::VALUE_NONE;
9798
if (false !== $self->default) {
9899
$self->mode |= InputOption::VALUE_NEGATABLE;
99100
}
101+
} elseif ('array' === $self->typeName) {
102+
$self->mode = InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY;
100103
} else {
101-
$self->mode = $self->allowNull ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_REQUIRED;
102-
if ('array' === $self->typeName) {
103-
$self->mode |= InputOption::VALUE_IS_ARRAY;
104-
}
104+
$self->mode = InputOption::VALUE_REQUIRED;
105105
}
106106

107107
if (\is_array($self->suggestedValues) && !\is_callable($self->suggestedValues) && 2 === \count($self->suggestedValues) && ($instance = $parameter->getDeclaringFunction()->getClosureThis()) && $instance::class === $self->suggestedValues[0] && \is_callable([$instance, $self->suggestedValues[1]])) {
@@ -129,6 +129,14 @@ public function resolveValue(InputInterface $input): mixed
129129
{
130130
$value = $input->getOption($this->name);
131131

132+
if (null === $value && \in_array($this->typeName, self::ALLOWED_UNION_TYPES, true)) {
133+
return true;
134+
}
135+
136+
if ('array' === $this->typeName && $this->allowNull && [] === $value) {
137+
return null;
138+
}
139+
132140
if ('bool' !== $this->typeName) {
133141
return $value;
134142
}
@@ -139,4 +147,28 @@ public function resolveValue(InputInterface $input): mixed
139147

140148
return $value ?? $this->default;
141149
}
150+
151+
private function handleUnion(\ReflectionUnionType $type): self
152+
{
153+
$types = array_map(
154+
static fn(\ReflectionType $t) => $t instanceof \ReflectionNamedType ? $t->getName() : null,
155+
$type->getTypes(),
156+
);
157+
158+
sort($types);
159+
160+
$this->typeName = implode('|', array_filter($types));
161+
162+
if (!\in_array($this->typeName, self::ALLOWED_UNION_TYPES, true)) {
163+
throw new LogicException(\sprintf('The union type for parameter "$%s" is not supported as a command option. Only "%s" types are allowed.', $this->name, implode('", "', self::ALLOWED_UNION_TYPES)));
164+
}
165+
166+
if (false !== $this->default) {
167+
throw new LogicException(\sprintf('The option parameter "$%s" must have a default value of false.', $this->name));
168+
}
169+
170+
$this->mode = InputOption::VALUE_OPTIONAL;
171+
172+
return $this;
173+
}
142174
}

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

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,16 @@ public function testCommandInputOptionDefinition()
7979
#[Option(shortcut: 'v')] bool $verbose = false,
8080
#[Option(description: 'User groups')] array $groups = [],
8181
#[Option(suggestedValues: [self::class, 'getSuggestedRoles'])] array $roles = ['ROLE_USER'],
82+
#[Option] string|bool $opt = false,
8283
): int {
8384
return 0;
8485
});
8586

8687
$timeoutInputOption = $command->getDefinition()->getOption('idle');
8788
self::assertSame('idle', $timeoutInputOption->getName());
8889
self::assertNull($timeoutInputOption->getShortcut());
89-
self::assertTrue($timeoutInputOption->isValueOptional());
90+
self::assertTrue($timeoutInputOption->isValueRequired());
91+
self::assertFalse($timeoutInputOption->isValueOptional());
9092
self::assertFalse($timeoutInputOption->isNegatable());
9193
self::assertNull($timeoutInputOption->getDefault());
9294

@@ -120,6 +122,14 @@ public function testCommandInputOptionDefinition()
120122
self::assertTrue($rolesInputOption->hasCompletion());
121123
$rolesInputOption->complete(new CompletionInput(), $suggestions = new CompletionSuggestions());
122124
self::assertSame(['ROLE_ADMIN', 'ROLE_USER'], array_map(static fn (Suggestion $s) => $s->getValue(), $suggestions->getValueSuggestions()));
125+
126+
$optInputOption = $command->getDefinition()->getOption('opt');
127+
self::assertSame('opt', $optInputOption->getName());
128+
self::assertNull($optInputOption->getShortcut());
129+
self::assertFalse($optInputOption->isValueRequired());
130+
self::assertTrue($optInputOption->isValueOptional());
131+
self::assertFalse($optInputOption->isNegatable());
132+
self::assertFalse($optInputOption->getDefault());
123133
}
124134

125135
public function testInvalidArgumentType()
@@ -136,7 +146,7 @@ public function testInvalidArgumentType()
136146
public function testInvalidOptionType()
137147
{
138148
$command = new Command('foo');
139-
$command->setCode(function (#[Option] object $any) {});
149+
$command->setCode(function (#[Option] ?object $any = null) {});
140150

141151
$this->expectException(LogicException::class);
142152
$this->expectExceptionMessage('The type "object" of parameter "$any" is not supported as a command option. Only "string", "bool", "int", "float", "array" types are allowed.');
@@ -262,14 +272,30 @@ public function testNonBinaryInputOptions(array $parameters, array $e 6D4E xpected)
262272
$command = new Command('foo');
263273
$command->setCode(function (
264274
#[Option] string $a = '',
265-
#[Option] ?string $b = '',
266-
#[Option] array $c = [],
267-
#[Option] array $d = ['a', 'b'],
275+
#[Option] array $b = [],
276+
#[Option] array $c = ['a', 'b'],
277+
#[Option] bool|string $d = false,
278+
#[Option] ?string $e = null,
279+
#[Option] ?array $f = null,
280+
#[Option] int $g = 0,
281+
#[Option] ?int $h = null,
282+
#[Option] float $i = 0.0,
283+
#[Option] ?float $j = null,
284+
#[Option] bool|int $k = false,
285+
#[Option] bool|float $l = false,
268286
) use ($expected): int {
269287
$this->assertSame($expected[0], $a);
270288
$this->assertSame($expected[1], $b);
271289
$this->assertSame($expected[2], $c);
272290
$this->assertSame($expected[3], $d);
291+
$this->assertSame($expected[4], $e);
292+
$this->assertSame($expected[5], $f);
293+
$this->assertSame($expected[6], $g);
294+
$this->assertSame($expected[7], $h);
295+
$this->assertSame($expected[8], $i);
296+
$this->assertSame($expected[9], $j);
297+
$this->assertSame($expected[10], $k);
298+
$this->assertSame($expected[11], $l);
273299

274300
return 0;
275301
});
@@ -279,9 +305,18 @@ public function testNonBinaryInputOptions(array $parameters, array $expected)
279305

280306
public static function provideNonBinaryInputOptions(): \Generator
281307
{
282-
yield 'defaults' => [[], ['', '', [], ['a', 'b']]];
283-
yield 'with-value' => [['--a' => 'x', '--b' => 'y', '--c' => ['z'], '--d' => ['c', 'd']], ['x', 'y', ['z'], ['c', 'd']]];
284-
yield 'without-value' => [['--b' => null], ['', null, [], ['a', 'b']]];
308+
yield 'defaults' => [
309+
[],
310+
['', [], ['a', 'b'], false, null, null, 0, null, 0.0, null, false, false],
311+
];
312+
yield 'with-value' => [
313+
['--a' => 'x', '--b' => ['z'], '--c' => ['c', 'd'], '--d' => 'v', '--e' => 'w', '--f' => ['q'], '--g' => 1, '--h' => 2, '--i' => 3.1, '--j' => 4.2, '--k' => 5, '--l' => 6.3],
314+
['x', ['z'], ['c', 'd'], 'v', 'w', ['q'], 1, 2, 3.1, 4.2, 5, 6.3],
315+
];
316+
yield 'without-value' => [
317+
['--d' => null, '--k' => null, '--l' => null],
318+
['', [], ['a', 'b'], true, null, null, 0, null, 0.0, null, true, true],
319+
];
285320
}
286321

287322
/**
@@ -312,13 +347,29 @@ function (#[Option] ?bool $a = true) {},
312347
function (#[Option] ?bool $a = false) {},
313348
'The option parameter "$a" must not be nullable when it has a default boolean value.',
314349
];
315-
yield 'nullable-string' => [
316-
function (#[Option] ?string $a = null) {},
317-
'The option parameter "$a" must not have a default of null.',
350+
yield 'invalid-union-type' => [
351+
function (#[Option] array|bool $a = false) {},
352+
'The union type for parameter "$a" is not supported as a command option. Only "bool|string", "bool|int", "bool|float" types are allowed.',
353+
];
354+
yield 'union-type-cannot-allow-null' => [
355+
function (#[Option] string|bool|null $a = null) {},
356+
'The union type for parameter "$a" is not supported as a command option. Only "bool|string", "bool|int", "bool|float" types are allowed.',
357+
];
358+
yield 'union-type-default-true' => [
359+
function (#[Option] string|bool $a = true) {},
360+
'The option parameter "$a" must have a default value of false.',
361+
];
362+
yield 'union-type-default-string' => [
363+
function (#[Option] string|bool $a = 'foo') {},
364+
'The option parameter "$a" must have a default value of false.',
365+
];
366+
yield 'nullable-string-not-null-default' => [
367+
function (#[Option] ?string $a = 'foo') {},
368+
'The option parameter "$a" must either be not-nullable or have a default of null.',
318369
];
319-
yield 'nullable-array' => [
320-
function (#[Option] ?array $a = null) {},
321-
'The option parameter "$a" must not be nullable.',
370+
yield 'nullable-array-not-null-default' => [
371+
function (#[Option] ?array $a = []) {},
372+
'The option parameter "$a" must either be not-nullable or have a default of null.',
322373
];
323374
}
324375

0 commit comments

Comments
 (0)
0