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

Skip to content

Commit 2a19b9b

Browse files
committed
[Console] Simplify using invokable commands when the component is used standalone
1 parent e532750 commit 2a19b9b

File tree

3 files changed

+99
-2
lines changed

3 files changed

+99
-2
lines changed

src/Symfony/Component/Console/Application.php

+26-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Console;
1313

14+
use Symfony\Component\Console\Attribute\AsCommand;
1415
use Symfony\Component\Console\Command\Command;
1516
use Symfony\Component\Console\Command\CompleteCommand;
1617
use Symfony\Component\Console\Command\DumpCompletionCommand;
@@ -28,6 +29,7 @@
2829
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
2930
use Symfony\Component\Console\Exception\CommandNotFoundException;
3031
use Symfony\Component\Console\Exception\ExceptionInterface;
32+
use Symfony\Component\Console\Exception\InvalidArgumentException;
3133
use Symfony\Component\Console\Exception\LogicException;
3234
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
3335
use Symfony\Component\Console\Exception\RuntimeException;
@@ -520,12 +522,12 @@ public function register(string $name): Command
520522
*
521523
* If a Command is not enabled it will not be added.
522524
*
523-
* @param Command[] $commands An array of commands
525+
* @param callable|Command[] $commands An array of commands
524526
*/
525527
public function addCommands(array $commands): void
526528
{
527529
foreach ($commands as $command) {
528-
$this->add($command);
530+
$this->addCommand($command);
529531
}
530532
}
531533

@@ -565,6 +567,28 @@ public function add(Command $command): ?Command
565567
return $command;
566568
}
567569

570+
public function addCommand(callable|Command $command): ?Command
571+
{
572+
if ($command instanceof Command) {
573+
return $this->add($command);
574+
}
575+
576+
if (!\is_object($command) || $command instanceof \Closure) {
577+
throw new InvalidArgumentException(\sprintf('The command must be an instance of "%s" or an invokable object.', Command::class));
578+
}
579+
580+
/** @var AsCommand $attribute */
581+
$attribute = ((new \ReflectionObject($command))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance()
582+
?? throw new LogicException(\sprintf('The command must use the "%s" attribute.', AsCommand::class));
583+
584+
return $this->add(
585+
(new Command($attribute->name))
586+
->setDescription($attribute->description ?? '')
587+
->setHelp($attribute->help ?? '')
588+
->setCode($command)
589+
);
590+
}
591+
568592
/**
569593
* Returns a registered command by name or alias.
570594
*

src/Symfony/Component/Console/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ CHANGELOG
1515
* Deprecate returning a non-integer value from a `\Closure` function set via `Command::setCode()`
1616
* Mark `#[AsCommand]` attribute as `@final`
1717
* Add support for `SignalableCommandInterface` with invokable commands
18+
* Simplify using invokable commands when the component is used standalone
1819

1920
7.2
2021
---

src/Symfony/Component/Console/Tests/ApplicationTest.php

+72
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\Console\Attribute\AsCommand;
1717
use Symfony\Component\Console\Command\Command;
1818
use Symfony\Component\Console\Command\HelpCommand;
19+
use Symfony\Component\Console\Command\InvokableCommand;
1920
use Symfony\Component\Console\Command\LazyCommand;
2021
use Symfony\Component\Console\Command\SignalableCommandInterface;
2122
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
@@ -28,6 +29,8 @@
2829
use Symfony\Component\Console\Event\ConsoleSignalEvent;
2930
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
3031
use Symfony\Component\Console\Exception\CommandNotFoundException;
32+
use Symfony\Component\Console\Exception\InvalidArgumentException;
33+
use Symfony\Component\Console\Exception\LogicException;
3134
use Symfony\Component\Console\Exception\NamespaceNotFoundException;
3235
use Symfony\Component\Console\Helper\FormatterHelper;
3336
use Symfony\Component\Console\Helper\HelperSet;
@@ -239,6 +242,59 @@ public function testAddCommandWithEmptyConstructor()
239242
(new Application())->add(new \Foo5Command());
240243
}
241244

245+
public function testAddCommandWithExtendedCommand()
246+
{
247+
$application = new Application();
248+
$application->add($foo = new \FooCommand());
249+
$commands = $application->all();
250+
251+
$this->assertEquals($foo, $commands['foo:bar']);
252+
}
253+
254+
public function testAddCommandWithInvokableCommand()
255+
{
256+
$application = new Application();
257+
$application->addCommand($foo = new InvokableTestCommand());
258+
$commands = $application->all();
259+
260+
$this->assertInstanceOf(Command::class, $command = $commands['invokable']);
261+
$this->assertEquals(new InvokableCommand($command, $foo), (new \ReflectionObject($command))->getProperty('code')->getValue($command));
262+
}
263+
264+
public function testAddCommandWithInvokableExtendedCommand()
265+
{
266+
$application = new Application();
267+
$application->addCommand($foo = new InvokableExtendedTestCommand());
268+
$commands = $application->all();
269+
270+
$this->assertEquals($foo, $commands['invokable-extended']);
271+
}
272+
273+
/**
274+
* @dataProvider provideInvalidInvokableCommands
275+
*/
276+
public function testAddCommandThrowsExceptionOnInvalidCommand(callable $command, string $expectedException, string $expectedExceptionMessage)
277+
{
278+
$application = new Application();
279+
280+
$this->expectException($expectedException);
281+
$this->expectExceptionMessage($expectedExceptionMessage);
282+
283+
$application->addCommand($command);
284+
}
285+
286+
public static function provideInvalidInvokableCommands(): iterable
287+
{
288+
yield 'a function' => ['strlen', InvalidArgumentException::class, \sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)];
289+
yield 'a closure' => [function () {
290+
}, InvalidArgumentException::class, \sprintf('The command must be an instance of "%s" or an invokable object.', Command::class)];
291+
yield 'without the #[AsCommand] attribute' => [new class {
292+
public function __invoke()
293+
{
294+
}
295+
}, LogicException::class, \sprintf('The command must use the "%s" attribute.', AsCommand::class)];
296+
}
297+
242298
public function testHasGet()
243299
{
244300
$application = new Application();
@@ -2514,6 +2570,22 @@ public function isEnabled(): bool
25142570
}
25152571
}
25162572

2573+
#[AsCommand(name: 'invokable')]
2574+
class InvokableTestCommand
2575+
{
2576+
public function __invoke(): int
2577+
{
2578+
}
2579+
}
2580+
2581+
#[AsCommand(name: 'invokable-extended')]
2582+
class InvokableExtendedTestCommand extends Command
2583+
{
2584+
public function __invoke(): int
2585+
{
2586+
}
2587+
}
2588+
25172589
#[AsCommand(name: 'signal')]
25182590
class BaseSignableCommand extends Command
25192591
{

0 commit comments

Comments
 (0)