8000 [Console] Simplify using invokable commands when the component is use… · symfony/symfony@5cf2a0d · GitHub
[go: up one dir, main page]

Skip to content

Commit 5cf2a0d

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

File tree

3 files changed

+90
-0
lines changed

3 files changed

+90
-0
lines changed

src/Symfony/Component/Console/Application.php

Lines changed: 27 additions & 0 deletions
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;
@@ -565,6 +567,31 @@ public function add(Command $command): ?Command
565567
return $command;
566568
}
567569

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

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
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

Lines changed: 62 additions & 0 deletions
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,49 @@ public function testAddCommandWithEmptyConstructor()
239242
(new Application())->add(new \Foo5Command());
240243
}
241244

245+
public function testAddInvokable()
246+
{
247+
$application = new Application();
248+
$application->addInvokable($foo = new InvokableTestCommand());
249+
$commands = $application->all();
250+
251+
$this->assertInstanceOf(Command::class, $command = $commands['invokable']);
252+
$this->assertEquals(new InvokableCommand($command, $foo), (new \ReflectionObject($command))->getProperty('code')->getValue($command));
253+
}
254+
255+
public function testAddInvokableWithExtendedCommand()
256+
{
257+
$application = new Application();
258+
$application->addInvokable($foo = new InvokableExtendedTestCommand());
259+
$commands = $application->all();
260+
261+
$this->assertEquals($foo, $commands['invokable-extended']);
262+
}
263+
264+
/**
265+
* @dataProvider provideInvalidInvokableCommands
266+
*/
267+
public function testAddInvokableThrowsExceptionOnInvalidCommand(object $command, string $expectedException, string $expectedExceptionMessage)
268+
{
269+
$application = new Application();
270+
271+
$this->expectException($expectedException);
272+
$this->expectExceptionMessage($expectedExceptionMessage);
273+
274+
$application->addInvokable($command);
275+
}
276+
277+
public static function provideInvalidInvokableCommands(): iterable
278+
{
279+
yield 'not a callable' => [new class {}, InvalidArgumentException::class, 'The command must be an invokable object.'];
280+
yield 'a closure' => [function () {}, InvalidArgumentException::class, 'The command cannot be an anonymous function.'];
281+
yield 'without the #[AsCommand] attribute' => [new class {
282+
public function __invoke()
283+
{
284+
}
285+
}, LogicException::class, \sprintf('The command must use the "%s" attribute.', AsCommand::class)];
286+
}
287+
242288
public function testHasGet()
243289
{
244290
$application = new Application();
@@ -2514,6 +2560,22 @@ public function isEnabled(): bool
25142560
}
25152561
}
25162562

2563+
#[AsCommand(name: 'invokable')]
2564+
class InvokableTestCommand
2565+
{
2566+
public function __invoke(): int
2567+
{
2568+
}
2569+
}
2570+
2571+
#[AsCommand(name: 'invokable-extended')]
2572+
class InvokableExtendedTestCommand extends Command
2573+
{
2574+
public function __invoke(): int
2575+
{
2576+
}
2577+
}
2578+
25172579
#[AsCommand(name: 'signal')]
25182580
class BaseSignableCommand extends Command
25192581
{

0 commit comments

Comments
 (0)
0