Skip to content

Commit 2a73033

Browse files
gturpin-devbrendtinnocenzi
authored
feat(console): add name parameter to #[ConsoleArgument] (#617)
Co-authored-by: Brent Roose <[email protected]> Co-authored-by: Enzo Innocenzi <[email protected]>
1 parent 04000ac commit 2a73033

File tree

9 files changed

+157
-22
lines changed

9 files changed

+157
-22
lines changed

src/Tempest/Console/src/Actions/RenderConsoleCommand.php

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
use Tempest\Console\Console;
88
use Tempest\Console\ConsoleCommand;
9-
use Tempest\Reflection\ParameterReflector;
9+
use Tempest\Console\Input\ConsoleArgumentDefinition;
10+
use function Tempest\Support\str;
1011

1112
final readonly class RenderConsoleCommand
1213
{
@@ -18,8 +19,8 @@ public function __invoke(ConsoleCommand $consoleCommand): void
1819
{
1920
$parts = ["<em><strong>{$consoleCommand->getName()}</strong></em>"];
2021

21-
foreach ($consoleCommand->handler->getParameters() as $parameter) {
22-
$parts[] = $this->renderParameter($parameter);
22+
foreach ($consoleCommand->getArgumentDefinitions() as $argument) {
23+
$parts[] = $this->renderArgument($argument);
2324
}
2425

2526
if ($consoleCommand->description !== null && $consoleCommand->description !== '') {
@@ -29,22 +30,27 @@ public function __invoke(ConsoleCommand $consoleCommand): void
2930
$this->console->writeln(' ' . implode(' ', $parts));
3031
}
3132

32-
private function renderParameter(ParameterReflector $parameter): string
33+
private function renderArgument(ConsoleArgumentDefinition $argument): string
3334
{
34-
/** @phpstan-ignore-next-line */
35-
$type = $parameter->getType()?->getName();
36-
$optional = $parameter->isOptional();
37-
$defaultValue = strtolower(var_export($optional ? $parameter->getDefaultValue() : null, true));
38-
$name = "<em>{$parameter->getName()}</em>";
35+
$name = str($argument->name)
36+
->prepend('<em>')
37+
->append('</em>');
3938

40-
$asString = match($type) {
39+
$asString = match($argument->type) {
4140
'bool' => "<em>--</em>{$name}",
4241
default => $name,
4342
};
4443

45-
return match($optional) {
46-
true => "[{$asString}={$defaultValue}]",
47-
false => "<{$asString}>",
44+
if (! $argument->hasDefault) {
45+
return "<{$asString}>";
46+
}
47+
48+
return match (true) {
49+
$argument->default === true => "[{$asString}=true]",
50+
$argument->default === false => "[{$asString}=false]",
51+
is_null($argument->default) => "[{$asString}=null]",
52+
is_array($argument->default) => "[{$asString}=array]",
53+
default => "[{$asString}={$argument->default}]"
4854
};
4955
}
5056
}

src/Tempest/Console/src/ConsoleArgument.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
final readonly class ConsoleArgument
1111
{
1212
public function __construct(
13+
public ?string $name = null,
1314
public ?string $description = null,
1415
public string $help = '',
1516
public array $aliases = [],

src/Tempest/Console/src/Input/ConsoleArgumentDefinition.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Tempest\Console\ConsoleArgument;
88
use Tempest\Reflection\ParameterReflector;
9+
use function Tempest\Support\str;
910

1011
final readonly class ConsoleArgumentDefinition
1112
{
@@ -24,13 +25,14 @@ public function __construct(
2425
public static function fromParameter(ParameterReflector $parameter): ConsoleArgumentDefinition
2526
{
2627
$attribute = $parameter->getAttribute(ConsoleArgument::class);
27-
2828
$type = $parameter->getType();
29+
$default = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
30+
$boolean = $type->getName() === 'bool' || is_bool($default);
2931

3032
return new ConsoleArgumentDefinition(
31-
name: $parameter->getName(),
33+
name: static::normalizeName($attribute?->name ?? $parameter->getName(), boolean: $boolean),
3234
type: $type->getName(),
33-
default: $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null,
35+
default: $default,
3436
hasDefault: $parameter->isDefaultValueAvailable(),
3537
position: $parameter->getPosition(),
3638
description: $attribute?->description,
@@ -50,11 +52,22 @@ public function matchesArgument(ConsoleInputArgument $argument): bool
5052
}
5153

5254
foreach ([$this->name, ...$this->aliases] as $match) {
53-
if ($argument->matches($match)) {
55+
if ($argument->matches(static::normalizeName($match, $this->type === 'bool'))) {
5456
return true;
5557
}
5658
}
5759

5860
return false;
5961
}
62+
63+
private static function normalizeName(string $name, bool $boolean): string
64+
{
65+
$normalizedName = str($name)->kebab();
66+
67+
if ($boolean) {
68+
$normalizedName->replaceStart('no-', '');
69+
}
70+
71+
return $normalizedName->toString();
72+
}
6073
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Fixtures\Console;
6+
7+
use Tempest\Console\ConsoleArgument;
8+
use Tempest\Console\ConsoleCommand;
9+
use Tempest\Console\HasConsole;
10+
11+
final readonly class CommandWithArgumentName
12+
{
13+
use HasConsole;
14+
15+
#[ConsoleCommand(name: 'command-with-argument-name')]
16+
public function __invoke(
17+
#[ConsoleArgument(name: 'new-name')]
18+
string $input,
19+
#[ConsoleArgument(name: 'new-flag')]
20+
bool $flag = false,
21+
): void {
22+
$this->writeln($input);
23+
$this->writeln($flag ? 'true' : 'false');
24+
}
25+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Fixtures\Console;
6+
7+
use Tempest\Console\ConsoleArgument;
8+
9+
// tests/Integration/Console/Input/ConsoleArgumentDefinitionTest.php
10+
final readonly class CommandWithDifferentArguments
11+
{
12+
public function __invoke(
13+
string $string,
14+
string $camelCaseString,
15+
#[ConsoleArgument(name: 'my-kebab-string')]
16+
string $renamedKebabString,
17+
#[ConsoleArgument(name: 'myCamelString')]
18+
string $renamedCamelString,
19+
bool $bool,
20+
bool $camelCaseBool,
21+
string $camelCaseStringWithDefault = 'foo',
22+
bool $camelCaseBoolWithTrueDefault = true,
23+
bool $camelCaseBoolWithFalseDefault = false,
24+
): void {
25+
}
26+
}

tests/Fixtures/Console/Hello.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Tests\Tempest\Fixtures\Console;
66

77
use Tempest\Console\Console;
8+
use Tempest\Console\ConsoleArgument;
89
use Tempest\Console\ConsoleCommand;
910

1011
final readonly class Hello
@@ -23,8 +24,14 @@ public function world(string $input): void
2324
}
2425

2526
#[ConsoleCommand]
26-
public function test(?int $optionalValue = null, bool $flag = false): void
27-
{
27+
public function test(
28+
#[ConsoleArgument]
29+
?int $optionalValue = null,
30+
#[ConsoleArgument(
31+
name: 'custom-flag',
32+
)]
33+
bool $flag = false
34+
): void {
2835
$value = $optionalValue ?? 'null';
2936

3037
$this->console->info("{$value}");

tests/Integration/Console/ConsoleArgumentBagTest.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,7 @@ public function test_positional_vs_named_input(): void
5454
$this->console
5555
->call('complex a --c=c --b=b --flag')
5656
->assertContains('abc')
57-
->assertContains('true')
58-
;
57+
->assertContains('true');
5958
}
6059

6160
public function test_combined_flags(): void
@@ -138,4 +137,12 @@ public function test_negative_input(string $name, bool $expected): void
138137

139138
$this->assertSame($expected, $bag->findFor($definition)->value);
140139
}
140+
141+
public function test_name_mapping(): void
142+
{
143+
$this->console
144+
->call('command-with-argument-name --new-name=foo --new-flag')
145+
->assertSee('foo')
146+
->assertSee('true');
147+
}
141148
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Integration\Console\Input;
6+
7+
use PHPUnit\Framework\Attributes\TestWith;
8+
use PHPUnit\Framework\TestCase;
9+
use RuntimeException;
10+
use Tempest\Console\Input\ConsoleArgumentDefinition;
11+
use Tempest\Reflection\ClassReflector;
12+
use Tempest\Reflection\ParameterReflector;
13+
use Tests\Tempest\Fixtures\Console\CommandWithDifferentArguments;
14+
15+
/**
16+
* @internal
17+
*/
18+
final class ConsoleArgumentDefinitionTest extends TestCase
19+
{
20+
#[TestWith(['string', 'string', 'string', null])]
21+
#[TestWith(['bool', 'bool', 'bool', null])]
22+
#[TestWith(['camelCaseString', 'camel-case-string', 'string', null])]
23+
#[TestWith(['camelCaseBool', 'camel-case-bool', 'bool', null])]
24+
#[TestWith(['renamedCamelString', 'my-camel-string', 'string', null])]
25+
#[TestWith(['renamedKebabString', 'my-kebab-string', 'string', null])]
26+
#[TestWith(['camelCaseBool', 'camel-case-bool', 'bool', null])]
27+
#[TestWith(['camelCaseStringWithDefault', 'camel-case-string-with-default', 'string', 'foo'])]
28+
#[TestWith(['camelCaseBoolWithTrueDefault', 'camel-case-bool-with-true-default', 'bool', true])]
29+
#[TestWith(['camelCaseBoolWithFalseDefault', 'camel-case-bool-with-false-default', 'bool', false])]
30+
public function test_parse_named_arguments_with_types_and_defaults(string $originalParameter, string $expectedName, string $expectedType, mixed $expectedDefault): void
31+
{
32+
$definition = ConsoleArgumentDefinition::fromParameter($this->getParameter($originalParameter));
33+
$this->assertSame($expectedName, $definition->name);
34+
$this->assertSame($expectedType, $definition->type);
35+
$this->assertSame($expectedDefault, $definition->default);
36+
}
37+
38+
private function getParameter(string $name): ParameterReflector
39+
{
40+
$reflector = new ClassReflector(CommandWithDifferentArguments::class);
41+
42+
foreach ($reflector->getMethod('__invoke')->getParameters() as $parameter) {
43+
if ($parameter->getName() === $name) {
44+
return $parameter;
45+
}
46+
}
47+
48+
throw new RuntimeException("Parameter not found: {$name}");
49+
}
50+
}

tests/Integration/Console/Middleware/OverviewMiddlewareTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function test_overview(): void
2020
->assertContains('Hello')
2121
->assertDoesNotContain('hidden')
2222
->assertContains('hello:world <input>')
23-
->assertContains('hello:test [optionalValue=null] [--flag=false] - description')
23+
->assertContains('hello:test [optional-value=null] [--flag=false] - description')
2424
->assertContains('testcommand:test');
2525
}
2626

0 commit comments

Comments
 (0)