From a10ae7f443bfa260ef4218f5cd8e72f47b7705c0 Mon Sep 17 00:00:00 2001 From: Ruud Kamphuis Date: Fri, 20 Jun 2025 09:25:10 +0200 Subject: [PATCH] Add support for Invokable Commands in CommandTester --- src/Symfony/Component/Console/Application.php | 17 +------------- src/Symfony/Component/Console/CHANGELOG.md | 3 ++- .../Component/Console/Command/Command.php | 23 ++++++++++++++++++- .../Console/Tester/CommandTester.php | 5 +++- .../Console/Tests/ApplicationTest.php | 12 +++------- .../Console/Tests/Command/CommandTest.php | 8 +++++++ .../InvokableExtendingCommandTestCommand.php | 15 ++++++++++++ .../Tests/Fixtures/InvokableTestCommand.php | 15 ++++++++++++ .../Tests/Tester/CommandTesterTest.php | 22 ++++++++++++++++++ 9 files changed, 92 insertions(+), 28 deletions(-) create mode 100644 src/Symfony/Component/Console/Tests/Fixtures/InvokableExtendingCommandTestCommand.php create mode 100644 src/Symfony/Component/Console/Tests/Fixtures/InvokableTestCommand.php diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 6034b78f59b5a..fa3c381cfa233 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -552,22 +552,7 @@ public function addCommand(callable|Command $command): ?Command $this->init(); if (!$command instanceof Command) { - if (!\is_object($command) || $command instanceof \Closure) { - throw new InvalidArgumentException(\sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)); - } - - /** @var AsCommand $attribute */ - $attribute = ((new \ReflectionObject($command))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance() - ?? throw new LogicException(\sprintf('The command must use the "%s" attribute.', AsCommand::class)); - - $command = (new Command($attribute->name)) - ->setDescription($attribute->description ?? '') - ->setHelp($attribute->help ?? '') - ->setCode($command); - - foreach ($attribute->usages as $usage) { - $command->addUsage($usage); - } + $command = new Command(null, $command); } $command->setApplication($this); diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 1922e6562f130..722045091ff49 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -8,7 +8,8 @@ CHANGELOG * Introduce `Symfony\Component\Console\Application::addCommand()` to simplify using invokable commands when the component is used standalone * Deprecate `Symfony\Component\Console\Application::add()` in favor of `Symfony\Component\Console\Application::addCommand()` * Add `BackedEnum` support with `#[Argument]` and `#[Option]` inputs in invokable commands - * Allow Usages to be specified via #[AsCommand] attribute. + * Allow Usages to be specified via `#[AsCommand]` attribute. + * Allow passing invokable commands to `Symfony\Component\Console\Tester\CommandTester` 7.3 --- diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index 0ae82cf9ab57c..9e6e41ec9cda5 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -87,10 +87,31 @@ public static function getDefaultDescription(): ?string * * @throws LogicException When the command name is empty */ - public function __construct(?string $name = null) + public function __construct(?string $name = null, ?callable $code = null) { $this->definition = new InputDefinition(); + if ($code !== null) { + if (!\is_object($code) || $code instanceof \Closure) { + throw new InvalidArgumentException(\sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)); + } + + /** @var AsCommand $attribute */ + $attribute = ((new \ReflectionObject($code))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance() + ?? throw new LogicException(\sprintf('The command must use the "%s" attribute.', AsCommand::class)); + + $this->setName($name ?? $attribute->name) + ->setDescription($attribute->description ?? '') + ->setHelp($attribute->help ?? '') + ->setCode($code); + + foreach ($attribute->usages as $usage) { + $this->addUsage($usage); + } + + return; + } + $attribute = ((new \ReflectionClass(static::class))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance(); if (null === $name) { diff --git a/src/Symfony/Component/Console/Tester/CommandTester.php b/src/Symfony/Component/Console/Tester/CommandTester.php index d39cde7f6e8e2..714d88ad51dea 100644 --- a/src/Symfony/Component/Console/Tester/CommandTester.php +++ b/src/Symfony/Component/Console/Tester/CommandTester.php @@ -24,9 +24,12 @@ class CommandTester { use TesterTrait; + private Command $command; + public function __construct( - private Command $command, + callable|Command $command, ) { + $this->command = $command instanceof Command ? $command : new Command(null, $command); } /** diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index e5d16d7fe3b99..1a730a95b3aa1 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -48,6 +48,8 @@ use Symfony\Component\Console\SignalRegistry\SignalRegistry; use Symfony\Component\Console\Terminal; use Symfony\Component\Console\Tester\ApplicationTester; +use Symfony\Component\Console\Tests\Fixtures\InvokableExtendingCommandTestCommand; +use Symfony\Component\Console\Tests\Fixtures\InvokableTestCommand; use Symfony\Component\Console\Tests\Fixtures\MockableAppliationWithTerminalWidth; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -257,7 +259,7 @@ public function testAddCommandWithInvokableCommand() $application->addCommand($foo = new InvokableTestCommand()); $commands = $application->all(); - $this->assertInstanceOf(Command::class, $command = $commands['invokable']); + $this->assertInstanceOf(Command::class, $command = $commands['invokable:test']); $this->assertEquals(new InvokableCommand($command, $foo), (new \ReflectionObject($command))->getProperty('code')->getValue($command)); } @@ -2570,14 +2572,6 @@ public function isEnabled(): bool } } -#[AsCommand(name: 'invokable')] -class InvokableTestCommand -{ - public function __invoke(): int - { - } -} - #[AsCommand(name: 'invokable-extended')] class InvokableExtendedTestCommand extends Command { diff --git a/src/Symfony/Component/Console/Tests/Command/CommandTest.php b/src/Symfony/Component/Console/Tests/Command/CommandTest.php index 44e8996293f8a..a4a719b3d10ab 100644 --- a/src/Symfony/Component/Console/Tests/Command/CommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/CommandTest.php @@ -27,6 +27,7 @@ use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Console\Tests\Fixtures\InvokableTestCommand; class CommandTest extends TestCase { @@ -304,6 +305,13 @@ public function testRunInteractive() $this->assertEquals('interact called'.\PHP_EOL.'execute called'.\PHP_EOL, $tester->getDisplay(), '->run() calls the interact() method if the input is interactive'); } + public function testInvokableCommand() + { + $tester = new CommandTester(new InvokableTestCommand()); + + $this->assertSame(Command::SUCCESS, $tester->execute([])); + } + public function testRunNonInteractive() { $tester = new CommandTester(new \TestCommand()); diff --git a/src/Symfony/Component/Console/Tests/Fixtures/InvokableExtendingCommandTestCommand.php b/src/Symfony/Component/Console/Tests/Fixtures/InvokableExtendingCommandTestCommand.php new file mode 100644 index 0000000000000..724951608c23f --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Fixtures/InvokableExtendingCommandTestCommand.php @@ -0,0 +1,15 @@ +assertSame('foo', $tester->getErrorOutput()); } + + public function testAInvokableCommand() + { + $command = new InvokableTestCommand(); + + $tester = new CommandTester($command); + $tester->execute([]); + + $tester->assertCommandIsSuccessful(); + } + + public function testAInvokableExtendedCommand() + { + $command = new InvokableExtendingCommandTestCommand(); + + $tester = new CommandTester($command); + $tester->execute([]); + + $tester->assertCommandIsSuccessful(); + } }