Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions src/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ abstract class Command
* Argument definitions.
*
* @var array<int,array<string,mixed>> Definitions keyed by position, each
* with optional "type", "required" and "default" keys
* with optional "name", "type", "required", "default" and "description" keys
*/
protected array $argumentDefinitions = [];
/**
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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<string,mixed> $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.
*
Expand Down
3 changes: 2 additions & 1 deletion src/Commands/Help.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 !== '') {
Expand Down
57 changes: 57 additions & 0 deletions tests/ValidationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading