From dfec5bcbc270cfef53a5636d9bc829270b61693c Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sat, 17 May 2025 19:37:35 -0400 Subject: [PATCH] [Console] Improve invokable command attribute exceptions The exception messages now include the method: > The option parameter "$a" of "App\Command\SendSalesReportsCommand::__invoke()" must declare a default value. --- .../Component/Console/Attribute/Argument.php | 11 +++++++-- .../Component/Console/Attribute/Option.php | 21 ++++++++++------ .../Tests/Command/InvokableCommandTest.php | 24 ++++++++++--------- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/Symfony/Component/Console/Attribute/Argument.php b/src/Symfony/Component/Console/Attribute/Argument.php index 22bfbf48b762..e6a94d2f10e4 100644 --- a/src/Symfony/Component/Console/Attribute/Argument.php +++ b/src/Symfony/Component/Console/Attribute/Argument.php @@ -26,6 +26,7 @@ class Argument private string|bool|int|float|array|null $default = null; private array|\Closure $suggestedValues; private ?int $mode = null; + private string $function = ''; /** * Represents a console command definition. @@ -52,17 +53,23 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self return null; } + if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) { + $self->function = $function->class.'::'.$function->name; + } else { + $self->function = $function->name; + } + $type = $parameter->getType(); $name = $parameter->getName(); if (!$type instanceof \ReflectionNamedType) { - throw new LogicException(\sprintf('The parameter "$%s" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $name)); + throw new LogicException(\sprintf('The parameter "$%s" of "%s()" must have a named type. Untyped, Union or Intersection types are not supported for command arguments.', $name, $self->function)); } $parameterTypeName = $type->getName(); if (!\in_array($parameterTypeName, self::ALLOWED_TYPES, true)) { - throw new LogicException(\sprintf('The type "%s" of parameter "$%s" is not supported as a command argument. Only "%s" types are allowed.', $parameterTypeName, $name, implode('", "', self::ALLOWED_TYPES))); + throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command argument. Only "%s" types are allowed.', $parameterTypeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES))); } if (!$self->name) { diff --git a/src/Symfony/Component/Console/Attribute/Option.php b/src/Symfony/Component/Console/Attribute/Option.php index 19c82317033c..2f0256b17765 100644 --- a/src/Symfony/Component/Console/Attribute/Option.php +++ b/src/Symfony/Component/Console/Attribute/Option.php @@ -29,6 +29,7 @@ class Option private ?int $mode = null; private string $typeName = ''; private bool $allowNull = false; + private string $function = ''; /** * Represents a console command --option definition. @@ -57,11 +58,17 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self return null; } + if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) { + $self->function = $function->class.'::'.$function->name; + } else { + $self->function = $function->name; + } + $name = $parameter->getName(); $type = $parameter->getType(); if (!$parameter->isDefaultValueAvailable()) { - throw new LogicException(\sprintf('The option parameter "$%s" must declare a default value.', $name)); + throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must declare a default value.', $name, $self->function)); } if (!$self->name) { @@ -76,21 +83,21 @@ public static function tryFrom(\ReflectionParameter $parameter): ?self } if (!$type instanceof \ReflectionNamedType) { - throw new LogicException(\sprintf('The parameter "$%s" must have a named type. Untyped or Intersection types are not supported for command options.', $name)); + throw new LogicException(\sprintf('The parameter "$%s" of "%s()" must have a named type. Untyped or Intersection types are not supported for command options.', $name, $self->function)); } $self->typeName = $type->getName(); if (!\in_array($self->typeName, self::ALLOWED_TYPES, true)) { - 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))); + throw new LogicException(\sprintf('The type "%s" on parameter "$%s" of "%s()" is not supported as a command option. Only "%s" types are allowed.', $self->typeName, $name, $self->function, implode('", "', self::ALLOWED_TYPES))); } if ('bool' === $self->typeName && $self->allowNull && \in_array($self->default, [true, false], true)) { - throw new LogicException(\sprintf('The option parameter "$%s" must not be nullable when it has a default boolean value.', $name)); + throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must not be nullable when it has a default boolean value.', $name, $self->function)); } if ($self->allowNull && null !== $self->default) { - throw new LogicException(\sprintf('The option parameter "$%s" must either be not-nullable or have a default of null.', $name)); + throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must either be not-nullable or have a default of null.', $name, $self->function)); } if ('bool' === $self->typeName) { @@ -160,11 +167,11 @@ private function handleUnion(\ReflectionUnionType $type): self $this->typeName = implode('|', array_filter($types)); if (!\in_array($this->typeName, self::ALLOWED_UNION_TYPES, true)) { - 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))); + throw new LogicException(\sprintf('The union type for parameter "$%s" of "%s()" is not supported as a command option. Only "%s" types are allowed.', $this->name, $this->function, implode('", "', self::ALLOWED_UNION_TYPES))); } if (false !== $this->default) { - throw new LogicException(\sprintf('The option parameter "$%s" must have a default value of false.', $this->name)); + throw new LogicException(\sprintf('The option parameter "$%s" of "%s()" must have a default value of false.', $this->name, $this->function)); } $this->mode = InputOption::VALUE_OPTIONAL; diff --git a/src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php b/src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php index 917e2f88f165..9a95089a3c0e 100644 --- a/src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/InvokableCommandTest.php @@ -27,6 +27,8 @@ class InvokableCommandTest extends TestCase { + private const FUNCTION_NAME = 'Symfony\Component\Console\Tests\Command\InvokableCommandTest::Symfony\Component\Console\Tests\Command\{closure}()'; + public function testCommandInputArgumentDefinition() { $command = new Command('foo'); @@ -138,7 +140,7 @@ public function testInvalidArgumentType() $command->setCode(function (#[Argument] object $any) {}); $this->expectException(LogicException::class); - $this->expectExceptionMessage('The type "object" of parameter "$any" is not supported as a command argument. Only "string", "bool", "int", "float", "array" types are allowed.'); + $this->expectExceptionMessage(\sprintf('The type "object" on parameter "$any" of "%s" is not supported as a command argument. Only "string", "bool", "int", "float", "array" types are allowed.', self::FUNCTION_NAME)); $command->getDefinition(); } @@ -149,7 +151,7 @@ public function testInvalidOptionType() $command->setCode(function (#[Option] ?object $any = null) {}); $this->expectException(LogicException::class); - $this->expectExceptionMessage('The type "object" of parameter "$any" is not supported as a command option. Only "string", "bool", "int", "float", "array" types are allowed.'); + $this->expectExceptionMessage(\sprintf('The type "object" on parameter "$any" of "%s" is not supported as a command option. Only "string", "bool", "int", "float", "array" types are allowed.', self::FUNCTION_NAME)); $command->getDefinition(); } @@ -337,39 +339,39 @@ public static function provideInvalidOptionDefinitions(): \Generator { yield 'no-default' => [ function (#[Option] string $a) {}, - 'The option parameter "$a" must declare a default value.', + \sprintf('The option parameter "$a" of "%s" must declare a default value.', self::FUNCTION_NAME), ]; yield 'nullable-bool-default-true' => [ function (#[Option] ?bool $a = true) {}, - 'The option parameter "$a" must not be nullable when it has a default boolean value.', + \sprintf('The option parameter "$a" of "%s" must not be nullable when it has a default boolean value.', self::FUNCTION_NAME), ]; yield 'nullable-bool-default-false' => [ function (#[Option] ?bool $a = false) {}, - 'The option parameter "$a" must not be nullable when it has a default boolean value.', + \sprintf('The option parameter "$a" of "%s" must not be nullable when it has a default boolean value.', self::FUNCTION_NAME), ]; yield 'invalid-union-type' => [ function (#[Option] array|bool $a = false) {}, - 'The union type for parameter "$a" is not supported as a command option. Only "bool|string", "bool|int", "bool|float" types are allowed.', + \sprintf('The union type for parameter "$a" of "%s" is not supported as a command option. Only "bool|string", "bool|int", "bool|float" types are allowed.', self::FUNCTION_NAME), ]; yield 'union-type-cannot-allow-null' => [ function (#[Option] string|bool|null $a = null) {}, - 'The union type for parameter "$a" is not supported as a command option. Only "bool|string", "bool|int", "bool|float" types are allowed.', + \sprintf('The union type for parameter "$a" of "%s" is not supported as a command option. Only "bool|string", "bool|int", "bool|float" types are allowed.', self::FUNCTION_NAME), ]; yield 'union-type-default-true' => [ function (#[Option] string|bool $a = true) {}, - 'The option parameter "$a" must have a default value of false.', + \sprintf('The option parameter "$a" of "%s" must have a default value of false.', self::FUNCTION_NAME), ]; yield 'union-type-default-string' => [ function (#[Option] string|bool $a = 'foo') {}, - 'The option parameter "$a" must have a default value of false.', + \sprintf('The option parameter "$a" of "%s" must have a default value of false.', self::FUNCTION_NAME), ]; yield 'nullable-string-not-null-default' => [ function (#[Option] ?string $a = 'foo') {}, - 'The option parameter "$a" must either be not-nullable or have a default of null.', + \sprintf('The option parameter "$a" of "%s" must either be not-nullable or have a default of null.', self::FUNCTION_NAME), ]; yield 'nullable-array-not-null-default' => [ function (#[Option] ?array $a = []) {}, - 'The option parameter "$a" must either be not-nullable or have a default of null.', + \sprintf('The option parameter "$a" of "%s" must either be not-nullable or have a default of null.', self::FUNCTION_NAME), ]; }