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(); + } }
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: