Skip to content

Commit a6ba3b6

Browse files
authored
feat(console): allow calling console commands via fqcn (#824)
1 parent dbad109 commit a6ba3b6

File tree

12 files changed

+263
-61
lines changed

12 files changed

+263
-61
lines changed

src/Tempest/Console/src/Actions/ExecuteConsoleCommand.php

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,39 @@
44

55
namespace Tempest\Console\Actions;
66

7+
use Tempest\Console\ConsoleCommand;
78
use Tempest\Console\ConsoleConfig;
89
use Tempest\Console\ConsoleInputBuilder;
910
use Tempest\Console\ConsoleMiddlewareCallable;
1011
use Tempest\Console\ExitCode;
1112
use Tempest\Console\Initializers\Invocation;
1213
use Tempest\Console\Input\ConsoleArgumentBag;
1314
use Tempest\Container\Container;
15+
use Throwable;
1416

1517
final readonly class ExecuteConsoleCommand
1618
{
1719
public function __construct(
1820
private Container $container,
1921
private ConsoleConfig $consoleConfig,
2022
private ConsoleArgumentBag $argumentBag,
23+
private ResolveConsoleCommand $resolveConsoleCommand,
2124
) {
2225
}
2326

24-
public function __invoke(string $commandName): ExitCode|int
27+
public function __invoke(string|array $command, string|array $arguments = []): ExitCode|int
2528
{
26-
$callable = $this->getCallable($this->resolveCommandMiddleware($commandName));
29+
[$commandName, $arguments] = $this->resolveCommandAndArguments($command, $arguments);
2730

28-
$this->argumentBag->setCommandName($commandName);
31+
$consoleCommand = $this->resolveConsoleCommand($command) ?? $this->resolveConsoleCommand($commandName);
32+
$callable = $this->getCallable($consoleCommand?->middleware ?? []);
33+
34+
$this->argumentBag->setCommandName($consoleCommand?->getName() ?? $commandName);
35+
$this->argumentBag->addMany($arguments);
2936

3037
return $callable(new Invocation(
3138
argumentBag: $this->argumentBag,
39+
consoleCommand: $consoleCommand,
3240
));
3341
}
3442

@@ -37,9 +45,7 @@ private function getCallable(array $commandMiddleware): ConsoleMiddlewareCallabl
3745
$callable = new ConsoleMiddlewareCallable(function (Invocation $invocation) {
3846
$consoleCommand = $invocation->consoleCommand;
3947

40-
$handler = $consoleCommand->handler;
41-
42-
$consoleCommandClass = $this->container->get($handler->getDeclaringClass()->getName());
48+
$consoleCommandClass = $this->container->get($consoleCommand->handler->getDeclaringClass()->getName());
4349

4450
$inputBuilder = new ConsoleInputBuilder($consoleCommand, $invocation->argumentBag);
4551

@@ -62,10 +68,33 @@ private function getCallable(array $commandMiddleware): ConsoleMiddlewareCallabl
6268
return $callable;
6369
}
6470

65-
private function resolveCommandMiddleware(string $commandName): array
71+
private function resolveConsoleCommand(string|array $commandName): ?ConsoleCommand
6672
{
67-
$consoleCommand = $this->consoleConfig->commands[$commandName] ?? null;
73+
try {
74+
return ($this->resolveConsoleCommand)($commandName);
75+
} catch (Throwable) {
76+
return null;
77+
}
78+
}
79+
80+
/** @return array{string,array} */
81+
private function resolveCommandAndArguments(string|array $command, array $arguments = []): array
82+
{
83+
$commandName = $command;
84+
85+
if (is_array($command)) {
86+
$commandName = $command[0] ?? '';
87+
} elseif (str_contains($command, ' ')) {
88+
$commandName = explode(' ', $command)[0];
89+
$arguments = [
90+
...(array_slice(explode(' ', trim($command)), offset: 1)),
91+
...$arguments,
92+
];
93+
}
6894

69-
return $consoleCommand->middleware ?? [];
95+
return [
96+
$commandName,
97+
$arguments,
98+
];
7099
}
71100
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Console\Actions;
6+
7+
use Exception;
8+
use Tempest\Console\ConsoleCommand;
9+
use Tempest\Console\ConsoleConfig;
10+
11+
final class ResolveConsoleCommand
12+
{
13+
public function __construct(
14+
private readonly ConsoleConfig $consoleConfig,
15+
) {
16+
}
17+
18+
public function __invoke(array|string $command): ConsoleCommand
19+
{
20+
if (is_string($command) && array_key_exists($command, $this->consoleConfig->commands)) {
21+
return $this->consoleConfig->commands[$command];
22+
}
23+
24+
if (is_string($command) && class_exists($command)) {
25+
$command = [$command, '__invoke'];
26+
}
27+
28+
if (is_array($command)) {
29+
$command = array_find(
30+
array: $this->consoleConfig->commands,
31+
callback: fn (ConsoleCommand $consoleCommand) =>
32+
$consoleCommand->handler->getDeclaringClass()->getName() === $command[0]
33+
&& $consoleCommand->handler->getName() === $command[1],
34+
);
35+
36+
if ($command !== null) {
37+
return $command;
38+
}
39+
}
40+
41+
throw new Exception('Command not found.');
42+
}
43+
}

src/Tempest/Console/src/Console.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
interface Console
1313
{
14-
public function call(string $command): ExitCode|int;
14+
public function call(string|array $command, string|array $arguments = []): ExitCode|int;
1515

1616
public function readln(): string;
1717

src/Tempest/Console/src/GenericConsole.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ public function __construct(
4747
) {
4848
}
4949

50-
public function call(string $command): ExitCode|int
50+
public function call(string|array $command, string|array $arguments = []): ExitCode|int
5151
{
52-
return ($this->executeConsoleCommand)($command);
52+
return ($this->executeConsoleCommand)($command, $arguments);
5353
}
5454

5555
public function setComponentRenderer(InteractiveComponentRenderer $componentRenderer): self

src/Tempest/Console/src/Input/ConsoleArgumentBag.php

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,7 @@ public function __construct(array $arguments)
3636

3737
$this->path = [$cli, $commandName];
3838

39-
foreach ($arguments as $argument) {
40-
if (str_starts_with($argument, '-') && ! str_starts_with($argument, '--')) {
41-
$flags = str_split($argument);
42-
unset($flags[0]);
43-
44-
foreach ($flags as $flag) {
45-
$arguments[] = "-{$flag}";
46-
}
47-
}
48-
}
49-
50-
foreach (array_values($arguments) as $position => $argument) {
51-
$this->add(
52-
ConsoleInputArgument::fromString($argument, $position),
53-
);
54-
}
39+
$this->addMany($arguments);
5540
}
5641

5742
/**
@@ -159,6 +144,30 @@ public function add(ConsoleInputArgument $argument): self
159144
return $this;
160145
}
161146

147+
public function addMany(array $arguments): self
148+
{
149+
foreach ($arguments as $argument) {
150+
if (str_starts_with($argument, '-') && ! str_starts_with($argument, '--')) {
151+
$flags = str_split($argument);
152+
unset($flags[0]);
153+
154+
foreach ($flags as $flag) {
155+
$arguments[] = "-{$flag}";
156+
}
157+
}
158+
}
159+
160+
$position = count($this->arguments);
161+
162+
foreach (array_values($arguments) as $index => $argument) {
163+
$this->add(
164+
ConsoleInputArgument::fromString($argument, $position + $index),
165+
);
166+
}
167+
168+
return $this;
169+
}
170+
162171
public function getBinaryPath(): string
163172
{
164173
return PHP_BINARY;

src/Tempest/Console/src/Middleware/ResolveOrRescueMiddleware.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,30 @@
55
namespace Tempest\Console\Middleware;
66

77
use Tempest\Console\Actions\ExecuteConsoleCommand;
8+
use Tempest\Console\Actions\ResolveConsoleCommand;
89
use Tempest\Console\Console;
910
use Tempest\Console\ConsoleConfig;
1011
use Tempest\Console\ConsoleMiddleware;
1112
use Tempest\Console\ConsoleMiddlewareCallable;
1213
use Tempest\Console\ExitCode;
1314
use Tempest\Console\Initializers\Invocation;
15+
use Throwable;
1416

1517
final readonly class ResolveOrRescueMiddleware implements ConsoleMiddleware
1618
{
1719
public function __construct(
1820
private ConsoleConfig $consoleConfig,
1921
private Console $console,
2022
private ExecuteConsoleCommand $executeConsoleCommand,
23+
private ResolveConsoleCommand $resolveConsoleCommand,
2124
) {
2225
}
2326

2427
public function __invoke(Invocation $invocation, ConsoleMiddlewareCallable $next): ExitCode|int
2528
{
26-
$consoleCommand = $this->consoleConfig->commands[$invocation->argumentBag->getCommandName()] ?? null;
27-
28-
if (! $consoleCommand) {
29+
try {
30+
$consoleCommand = ($this->resolveConsoleCommand)($invocation->argumentBag->getCommandName());
31+
} catch (Throwable) {
2932
return $this->rescue($invocation->argumentBag->getCommandName());
3033
}
3134

src/Tempest/Console/src/Testing/ConsoleTester.php

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55
namespace Tempest\Console\Testing;
66

77
use Closure;
8-
use Exception;
98
use Fiber;
109
use PHPUnit\Framework\Assert;
1110
use Tempest\Console\Actions\ExecuteConsoleCommand;
1211
use Tempest\Console\Components\InteractiveComponentRenderer;
1312
use Tempest\Console\Console;
14-
use Tempest\Console\ConsoleCommand;
1513
use Tempest\Console\Exceptions\ConsoleErrorHandler;
1614
use Tempest\Console\ExitCode;
1715
use Tempest\Console\GenericConsole;
@@ -24,7 +22,6 @@
2422
use Tempest\Container\Container;
2523
use Tempest\Core\AppConfig;
2624
use Tempest\Highlight\Highlighter;
27-
use Tempest\Reflection\MethodReflector;
2825

2926
final class ConsoleTester
3027
{
@@ -43,7 +40,7 @@ public function __construct(
4340
) {
4441
}
4542

46-
public function call(string|Closure|array $command): self
43+
public function call(string|Closure|array $command, string|array $arguments = []): self
4744
{
4845
$clone = clone $this;
4946

@@ -82,30 +79,13 @@ public function call(string|Closure|array $command): self
8279
$clone->exitCode = $command($console) ?? ExitCode::SUCCESS;
8380
});
8481
} else {
85-
if (is_string($command) && class_exists($command)) {
86-
$command = [$command, '__invoke'];
87-
}
88-
89-
if (is_array($command) || class_exists($command)) {
90-
$handler = MethodReflector::fromParts(...$command);
91-
92-
$attribute = $handler->getAttribute(ConsoleCommand::class);
93-
94-
if ($attribute === null) {
95-
throw new Exception("Could not resolve console command from {$command[0]}::{$command[1]}");
96-
}
97-
98-
$attribute->setHandler($handler);
99-
100-
$command = $attribute->getName();
101-
}
102-
103-
$fiber = new Fiber(function () use ($command, $clone): void {
104-
$argumentBag = new ConsoleArgumentBag(['tempest', ...explode(' ', $command)]);
105-
106-
$clone->container->singleton(ConsoleArgumentBag::class, $argumentBag);
107-
108-
$clone->exitCode = ($this->container->get(ExecuteConsoleCommand::class))($argumentBag->getCommandName());
82+
$fiber = new Fiber(function () use ($command, $arguments, $clone): void {
83+
$clone->container->singleton(ConsoleArgumentBag::class, new ConsoleArgumentBag(['tempest']));
84+
$clone->exitCode = $this->container->invoke(
85+
ExecuteConsoleCommand::class,
86+
command: $command,
87+
arguments: $arguments,
88+
);
10989
});
11090
}
11191

tests/Integration/Console/Actions/ExecuteConsoleCommandTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
namespace Tests\Tempest\Integration\Console\Actions;
66

7+
use Tempest\Console\GenericConsole;
8+
use Tests\Tempest\Integration\Console\Fixtures\ArrayInputCommand;
9+
use Tests\Tempest\Integration\Console\Fixtures\CommandWithMiddleware;
710
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
811

912
/**
@@ -18,4 +21,36 @@ public function test_command_specific_middleware(): void
1821
->assertContains('from middleware')
1922
->assertContains('from command');
2023
}
24+
25+
public function test_command_specific_middleware_through_console(): void
26+
{
27+
$this->console
28+
->call(fn (GenericConsole $console) => $console->call('with:middleware'))
29+
->assertContains('from middleware')
30+
->assertContains('from command');
31+
}
32+
33+
public function test_call_command_by_class_name(): void
34+
{
35+
$this->console
36+
->call(CommandWithMiddleware::class)
37+
->assertContains('from middleware')
38+
->assertContains('from command');
39+
}
40+
41+
public function test_call_command_by_class_name_with_parameters(): void
42+
{
43+
$this->console
44+
->call(ArrayInputCommand::class, ['--input=a', '--input=b'])
45+
->assertSee('["a","b"]');
46+
}
47+
48+
public function test_command_with_positional_argument_with_space(): void
49+
{
50+
$this->markTestSkipped('Failing test.');
51+
52+
// $this->console
53+
// ->call('complex a "b b" c --flag')
54+
// ->assertSee('ab bc');
55+
}
2156
}

0 commit comments

Comments
 (0)