diff --git a/src/Command.php b/src/Command.php index 06b0783..582bd36 100644 --- a/src/Command.php +++ b/src/Command.php @@ -54,7 +54,7 @@ abstract class Command * Argument definitions. * * @var array> Definitions keyed by position, each - * with optional "type", "required" and "default" keys + * with optional "name", "type", "required", "default" and "description" keys */ protected array $argumentDefinitions = []; /** @@ -388,12 +388,13 @@ protected function validateDefinitions(array $definitions, array $values, string $label = $translator->render('cli', $label, []); } foreach ($definitions as $key => $definition) { + $displayName = $this->definitionLabel($key, $definition); $value = $values[$key] ?? null; if ($value === null || $value === false) { if (!empty($definition['required'])) { $errors[] = $translator - ? $translator->render('cli', 'validation.required', [$label, (string) $key]) - : $label . ' "' . $key . '" is required.'; + ? $translator->render('cli', 'validation.required', [$label, $displayName]) + : $label . ' "' . $displayName . '" is required.'; } continue; } @@ -409,8 +410,8 @@ protected function validateDefinitions(array $definitions, array $values, string // A boolean (true) means the option was passed without a // value, which is only valid for "flag" definitions $errors[] = $translator - ? $translator->render('cli', 'validation.type', [$label, (string) $key, $type]) - : $label . ' "' . $key . '" must be of type ' . $type . '.'; + ? $translator->render('cli', 'validation.type', [$label, $displayName, $type]) + : $label . ' "' . $displayName . '" must be of type ' . $type . '.'; } continue; } @@ -422,13 +423,32 @@ protected function validateDefinitions(array $definitions, array $values, string }; if (!$valid) { $errors[] = $translator - ? $translator->render('cli', 'validation.type', [$label, (string) $key, $type]) - : $label . ' "' . $key . '" must be of type ' . $type . '.'; + ? $translator->render('cli', 'validation.type', [$label, $displayName, $type]) + : $label . ' "' . $displayName . '" must be of type ' . $type . '.'; } } return $errors; } + /** + * Resolve the display label of a definition: its "name" when given, + * otherwise the array key (the argument position or option name). + * + * @param int|string $key The definition key + * @param array $definition The definition + * + * @return string + */ + #[Pure] + protected function definitionLabel(int | string $key, array $definition) : string + { + $name = $definition['name'] ?? null; + if (\is_string($name) && $name !== '') { + return $name; + } + return (string) $key; + } + /** * Tells if the command is active. * diff --git a/src/Commands/Help.php b/src/Commands/Help.php index 8329dc9..efff803 100644 --- a/src/Commands/Help.php +++ b/src/Commands/Help.php @@ -103,7 +103,8 @@ protected function showArguments(Command $command) : void ); $lastKey = \array_key_last($definitions); foreach ($definitions as $position => $definition) { - CLI::write(' ' . $position . ' ' . $this->describeDefinition($definition)); + $label = $this->definitionLabel($position, $definition); + CLI::write(' ' . $label . ' ' . $this->describeDefinition($definition)); $raw = $definition['description'] ?? null; $description = \is_string($raw) ? \trim($raw) : ''; if ($description !== '') { diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index b3acbc4..3b73729 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -215,6 +215,63 @@ public function testPassedValuesAreNotOverriddenByDefaults() : void self::assertSame('5', $this->console->getOption('count')); } + public function testNamedArgumentIsUsedInErrorMessages() : void + { + $command = new ValidatedCommandMock($this->console); + $command->setArgumentDefinitions([ + 0 => ['name' => 'environment', 'type' => 'int', 'required' => true], + ]); + $this->console->addCommand($command); + $this->console->exec('validated'); + self::assertStringContainsString('argument "environment" is required', Stderr::getContents()); + self::assertStringNotContainsString('"0"', Stderr::getContents()); + } + + public function testNamedArgumentTypeErrorsUseTheName() : void + { + $command = new ValidatedCommandMock($this->console); + $command->setArgumentDefinitions([ + 0 => ['name' => 'retries', 'type' => 'int'], + ]); + $this->console->addCommand($command); + $this->console->exec('validated abc'); + self::assertStringContainsString('argument "retries" must be of type int', Stderr::getContents()); + } + + public function testNamedOptionIsUsedInErrorMessages() : void + { + $command = new ValidatedCommandMock($this->console); + $command->setOptionDefinitions([ + 'count' => ['name' => 'attempts', 'type' => 'int', 'required' => true], + ]); + $this->console->addCommand($command); + $this->console->exec('validated 42'); + self::assertStringContainsString('option "attempts" is required', Stderr::getContents()); + } + + public function testNamedArgumentInTranslatedErrors() : void + { + $console = new ConsoleMock(new Language('pt-br')); + $command = new ValidatedCommandMock($console); + $command->setArgumentDefinitions([ + 0 => ['name' => 'ambiente', 'type' => 'int', 'required' => true], + ]); + $console->addCommand($command); + $console->exec('validated'); + self::assertStringContainsString('argumento "ambiente" é obrigatório.', Stderr::getContents()); + } + + public function testMissingNameFallsBackToTheKey() : void + { + $command = new ValidatedCommandMock($this->console); + $command->setArgumentDefinitions([ + 0 => ['type' => 'int', 'required' => true], + ]); + $this->console->addCommand($command); + $this->console->exec('validated'); + self::assertStringContainsString('argument "0" is required', Stderr::getContents()); + } + public function testApplyDefaultsDirectlyFillsTheConsole() : void { $command = new ValidatedCommandMock($this->console);