diff --git a/src/Tempest/Console/src/Actions/RenderConsoleCommand.php b/src/Tempest/Console/src/Actions/RenderConsoleCommand.php
index a5c8ab22f..f650398cd 100644
--- a/src/Tempest/Console/src/Actions/RenderConsoleCommand.php
+++ b/src/Tempest/Console/src/Actions/RenderConsoleCommand.php
@@ -4,6 +4,7 @@
namespace Tempest\Console\Actions;
+use BackedEnum;
use Tempest\Console\Console;
use Tempest\Console\ConsoleCommand;
use Tempest\Console\Input\ConsoleArgumentDefinition;
@@ -32,6 +33,10 @@ public function __invoke(ConsoleCommand $consoleCommand): void
private function renderArgument(ConsoleArgumentDefinition $argument): string
{
+ if ($argument->isBackedEnum()) {
+ return $this->renderEnumArgument($argument);
+ }
+
$name = str($argument->name)
->prepend('')
->append('');
@@ -53,4 +58,21 @@ private function renderArgument(ConsoleArgumentDefinition $argument): string
default => "[{$asString}={$argument->default}]"
};
}
+
+ private function renderEnumArgument(ConsoleArgumentDefinition $argument): string
+ {
+ $parts = array_map(
+ callback: fn (BackedEnum $case) => $case->value,
+ array: $argument->type::cases()
+ );
+
+ $partsAsString = ' {' . implode('|', $parts) . '}';
+ $line = "{$argument->name}";
+
+ if ($argument->hasDefault) {
+ return "[{$line}={$argument->default->value}{$partsAsString}]";
+ }
+
+ return "<{$line}{$partsAsString}>";
+ }
}
diff --git a/src/Tempest/Console/src/Exceptions/InvalidEnumArgument.php b/src/Tempest/Console/src/Exceptions/InvalidEnumArgument.php
new file mode 100644
index 000000000..a060d49f8
--- /dev/null
+++ b/src/Tempest/Console/src/Exceptions/InvalidEnumArgument.php
@@ -0,0 +1,40 @@
+ $argumentType
+ */
+ public function __construct(
+ private string $argumentName,
+ private string $argumentType,
+ private mixed $value,
+ ) {
+ }
+
+ public function render(Console $console): void
+ {
+ if (is_string($this->value) || is_numeric($this->value)) {
+ $value = "`{$this->value}`";
+ } else {
+ $value = 'of type `' . gettype($this->value) . '`';
+ }
+
+ $cases = array_map(
+ callback: fn (BackedEnum $case) => $case->value,
+ array: $this->argumentType::cases(),
+ );
+ $console->error("Invalid argument {$value} for `{$this->argumentName}` argument, valid values are: " . implode(', ', $cases));
+ }
+}
diff --git a/src/Tempest/Console/src/Input/ConsoleArgumentBag.php b/src/Tempest/Console/src/Input/ConsoleArgumentBag.php
index 24331cd22..73764aa2a 100644
--- a/src/Tempest/Console/src/Input/ConsoleArgumentBag.php
+++ b/src/Tempest/Console/src/Input/ConsoleArgumentBag.php
@@ -4,6 +4,8 @@
namespace Tempest\Console\Input;
+use Tempest\Console\Exceptions\InvalidEnumArgument;
+
final class ConsoleArgumentBag
{
/** @var ConsoleInputArgument[] */
@@ -93,7 +95,7 @@ public function findFor(ConsoleArgumentDefinition $argumentDefinition): ?Console
{
foreach ($this->arguments as $argument) {
if ($argumentDefinition->matchesArgument($argument)) {
- return $argument;
+ return $this->resolveArgumentValue($argumentDefinition, $argument);
}
}
@@ -108,6 +110,31 @@ public function findFor(ConsoleArgumentDefinition $argumentDefinition): ?Console
return null;
}
+ private function resolveArgumentValue(
+ ConsoleArgumentDefinition $argumentDefinition,
+ ConsoleInputArgument $argument,
+ ): ConsoleInputArgument {
+ if (! $argumentDefinition->isBackedEnum()) {
+ return $argument;
+ }
+
+ $resolved = $argumentDefinition->type::tryFrom($argument->value);
+
+ if ($resolved === null) {
+ throw new InvalidEnumArgument(
+ $argumentDefinition->name,
+ $argumentDefinition->type,
+ $argument->value,
+ );
+ }
+
+ return new ConsoleInputArgument(
+ name: $argumentDefinition->name,
+ position: $argumentDefinition->position,
+ value: $resolved,
+ );
+ }
+
public function findArrayFor(ConsoleArgumentDefinition $argumentDefinition): ?ConsoleInputArgument
{
$values = [];
diff --git a/src/Tempest/Console/src/Input/ConsoleArgumentDefinition.php b/src/Tempest/Console/src/Input/ConsoleArgumentDefinition.php
index 751d70988..f9e8a4474 100644
--- a/src/Tempest/Console/src/Input/ConsoleArgumentDefinition.php
+++ b/src/Tempest/Console/src/Input/ConsoleArgumentDefinition.php
@@ -4,6 +4,7 @@
namespace Tempest\Console\Input;
+use BackedEnum;
use Tempest\Console\ConsoleArgument;
use Tempest\Reflection\ParameterReflector;
use function Tempest\Support\str;
@@ -70,4 +71,9 @@ private static function normalizeName(string $name, bool $boolean): string
return $normalizedName->toString();
}
+
+ public function isBackedEnum(): bool
+ {
+ return is_subclass_of($this->type, BackedEnum::class);
+ }
}
diff --git a/tests/Integration/Console/ConsoleArgumentBagTest.php b/tests/Integration/Console/ConsoleArgumentBagTest.php
index 5935f3c79..12e815e92 100644
--- a/tests/Integration/Console/ConsoleArgumentBagTest.php
+++ b/tests/Integration/Console/ConsoleArgumentBagTest.php
@@ -5,8 +5,10 @@
namespace Tests\Tempest\Integration\Console;
use PHPUnit\Framework\Attributes\TestWith;
+use Tempest\Console\Exceptions\InvalidEnumArgument;
use Tempest\Console\Input\ConsoleArgumentBag;
use Tempest\Console\Input\ConsoleArgumentDefinition;
+use Tests\Tempest\Integration\Console\Fixtures\TestStringEnum;
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
/**
@@ -138,6 +140,49 @@ public function test_negative_input(string $name, bool $expected): void
$this->assertSame($expected, $bag->findFor($definition)->value);
}
+ public function test_backed_enum_input(): void
+ {
+ $argv = [
+ 'tempest',
+ 'test',
+ '--type=a',
+ ];
+
+ $bag = new ConsoleArgumentBag($argv);
+
+ $definition = new ConsoleArgumentDefinition(
+ name: 'type',
+ type: TestStringEnum::class,
+ default: null,
+ hasDefault: false,
+ position: 0,
+ );
+
+ $this->assertSame(TestStringEnum::A, $bag->findFor($definition)->value);
+ }
+
+ public function test_invalid_backed_enum_input(): void
+ {
+ $argv = [
+ 'tempest',
+ 'test',
+ '--type=invalid',
+ ];
+
+ $bag = new ConsoleArgumentBag($argv);
+
+ $definition = new ConsoleArgumentDefinition(
+ name: 'type',
+ type: TestStringEnum::class,
+ default: null,
+ hasDefault: false,
+ position: 0,
+ );
+
+ $this->expectException(InvalidEnumArgument::class);
+ $bag->findFor($definition);
+ }
+
public function test_name_mapping(): void
{
$this->console
diff --git a/tests/Integration/Console/Fixtures/MyConsole.php b/tests/Integration/Console/Fixtures/MyConsole.php
index a1d278810..55fc16bb1 100644
--- a/tests/Integration/Console/Fixtures/MyConsole.php
+++ b/tests/Integration/Console/Fixtures/MyConsole.php
@@ -14,6 +14,8 @@ final class MyConsole
)]
public function handle(
string $path,
+ TestStringEnum $type,
+ TestStringEnum $fallback = TestStringEnum::A,
int $times = 1,
bool $force = false,
): void {
diff --git a/tests/Integration/Console/Fixtures/TestStringEnum.php b/tests/Integration/Console/Fixtures/TestStringEnum.php
new file mode 100644
index 000000000..73d2947f1
--- /dev/null
+++ b/tests/Integration/Console/Fixtures/TestStringEnum.php
@@ -0,0 +1,12 @@
+assertSame(
- 'test [times=1] [--force=false] - description',
+ 'test [fallback=a {a|b|c}] [times=1] [--force=false] - description',
trim($output->getBufferWithoutFormatting()[0]),
);
}