Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Console] Simplify using invokable commands when the component is used standalone #60394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 7.3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions src/Symfony/Component/Console/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Console;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\CompleteCommand;
use Symfony\Component\Console\Command\DumpCompletionCommand;
Expand All @@ -28,6 +29,7 @@
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
use Symfony\Component\Console\Exception\RuntimeException;
Expand Down Expand Up @@ -520,12 +522,12 @@ public function register(string $name): Command
*
* If a Command is not enabled it will not be added.
*
* @param Command[] $commands An array of commands
* @param callable|Command[] $commands An array of commands
*/
public function addCommands(array $commands): void
{
foreach ($commands as $command) {
$this->add($command);
$this->addCommand($command);
}
}

Expand Down Expand Up @@ -565,6 +567,28 @@ public function add(Command $command): ?Command
return $command;
}

public function addCommand(callable|Command $command): ?Command
{
if ($command instanceof Command) {
return $this->add($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));

return $this->add(
(new Command($attribute->name))
->setDescription($attribute->description ?? '')
->setHelp($attribute->help ?? '')
->setCode($command)
);
}

/**
* Returns a registered command by name or alias.
*
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Console/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ CHANGELOG
* Deprecate returning a non-integer value from a `\Closure` function set via `Command::setCode()`
* Mark `#[AsCommand]` attribute as `@final`
* Add support for `SignalableCommandInterface` with invokable commands
* Simplify using invokable commands when the component is used standalone

7.2
---
Expand Down
72 changes: 72 additions & 0 deletions src/Symfony/Component/Console/Tests/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\HelpCommand;
use Symfony\Component\Console\Command\InvokableCommand;
use Symfony\Component\Console\Command\LazyCommand;
use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
Expand All @@ -28,6 +29,8 @@
use Symfony\Component\Console\Event\ConsoleSignalEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Helper\HelperSet;
Expand Down Expand Up @@ -239,6 +242,59 @@ public function testAddCommandWithEmptyConstructor()
(new Application())->add(new \Foo5Command());
}

public function testAddCommandWithExtendedCommand()
{
$application = new Application();
$application->add($foo = new \FooCommand());
$commands = $application->all();

$this->assertEquals($foo, $commands['foo:bar']);
}

public function testAddCommandWithInvokableCommand()
{
$application = new Application();
$application->addCommand($foo = new InvokableTestCommand());
$commands = $application->all();

$this->assertInstanceOf(Command::class, $command = $commands['invokable']);
$this->assertEquals(new InvokableCommand($command, $foo), (new \ReflectionObject($command))->getProperty('code')->getValue($command));
}

public function testAddCommandWithInvokableExtendedCommand()
{
$application = new Application();
$application->addCommand($foo = new InvokableExtendedTestCommand());
$commands = $application->all();

$this->assertEquals($foo, $commands['invokable-extended']);
}

/**
* @dataProvider provideInvalidInvokableCommands
*/
public function testAddCommandThrowsExceptionOnInvalidCommand(callable $command, string $expectedException, string $expectedExceptionMessage)
{
$application = new Application();

$this->expectException($expectedException);
$this->expectExceptionMessage($expectedExceptionMessage);

$application->addCommand($command);
}

public static function provideInvalidInvokableCommands(): iterable
{
yield 'a function' => ['strlen', InvalidArgumentException::class, \sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)];
yield 'a closure' => [function () {
}, InvalidArgumentException::class, \sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)];
yield 'without the #[AsCommand] attribute' => [new class {
public function __invoke()
{
}
}, LogicException::class, \sprintf('The command must use the "%s" attribute.', AsCommand::class)];
}

public function testHasGet()
{
$application = new Application();
Expand Down Expand Up @@ -2514,6 +2570,22 @@ public function isEnabled(): bool
}
}

#[AsCommand(name: 'invokable')]
class InvokableTestCommand
{
public function __invoke(): int
{
}
}

#[AsCommand(name: 'invokable-extended')]
class InvokableExtendedTestCommand extends Command
{
public function __invoke(): int
{
}
}

#[AsCommand(name: 'signal')]
class BaseSignableCommand extends Command
{
Expand Down