From e9a6b0af479d508131ddea97b96b999ff9ff07ee Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Fri, 10 Jan 2025 15:50:54 -0500 Subject: [PATCH] [Console] Add broader support for command "help" definition --- .../DependencyInjection/FrameworkExtension.php | 6 +++++- .../Component/Console/Attribute/AsCommand.php | 2 ++ src/Symfony/Component/Console/CHANGELOG.md | 1 + .../Component/Console/Command/Command.php | 4 ++++ .../AddConsoleCommandPass.php | 16 ++++++++-------- .../Console/Tests/Command/CommandTest.php | 3 ++- .../AddConsoleCommandPassTest.php | 14 +++++++++++--- 7 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index d435c83e0b2fc..9ef918161dce4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -611,7 +611,11 @@ public function load(array $configs, ContainerBuilder $container): void $container->registerForAutoconfiguration(AssetCompilerInterface::class) ->addTag('asset_mapper.compiler'); $container->registerAttributeForAutoconfiguration(AsCommand::class, static function (ChildDefinition $definition, AsCommand $attribute, \ReflectionClass $reflector): void { - $definition->addTag('console.command', ['command' => $attribute->name, 'description' => $attribute->description]); + $definition->addTag('console.command', [ + 'command' => $attribute->name, + 'description' => $attribute->description, + 'help' => $attribute->help, + ]); }); $container->registerForAutoconfiguration(Command::class) ->addTag('console.command'); diff --git a/src/Symfony/Component/Console/Attribute/AsCommand.php b/src/Symfony/Component/Console/Attribute/AsCommand.php index 6066d7c533d54..2147e71510436 100644 --- a/src/Symfony/Component/Console/Attribute/AsCommand.php +++ b/src/Symfony/Component/Console/Attribute/AsCommand.php @@ -22,12 +22,14 @@ class AsCommand * @param string|null $description The description of the command, displayed with the help page * @param string[] $aliases The list of aliases of the command. The command will be executed when using one of them (i.e. "cache:clean") * @param bool $hidden If true, the command won't be shown when listing all the available commands, but it can still be run as any other command + * @param string|null $help The help content of the command, displayed with the help page */ public function __construct( public string $name, public ?string $description = null, array $aliases = [], bool $hidden = false, + public ?string $help = null, ) { if (!$hidden && !$aliases) { return; diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index c37f4f100c96c..77b109b812410 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * Add support for invokable commands and add `#[Argument]` and `#[Option]` attributes to define input arguments and options * Deprecate not declaring the parameter type in callable commands defined through `setCode` method + * Add support for help definition via `AsCommand` attribute 7.2 --- diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index 6c85825d10de5..fb410d7f8adea 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -100,6 +100,10 @@ public function __construct(?string $name = null) $this->setDescription(static::getDefaultDescription() ?? ''); } + if ('' === $this->help && $attributes = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) { + $this->setHelp($attributes[0]->newInstance()->help ?? ''); + } + if (\is_callable($this)) { $this->code = new InvokableCommand($this, $this(...)); } diff --git a/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php index 78e355ad8a2c2..248ad3276a130 100644 --- a/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php +++ b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php @@ -53,6 +53,8 @@ public function process(ContainerBuilder $container): void $invokableRef = new Reference($id); $definition = $container->register($id .= '.command', $class = Command::class) ->addMethodCall('setCode', [$invokableRef]); + } else { + $invokableRef = null; } $aliases = $tags[0]['command'] ?? str_replace('%', '%%', $class::getDefaultName() ?? ''); @@ -75,6 +77,7 @@ public function process(ContainerBuilder $container): void } $description = $tags[0]['description'] ?? null; + $help = $tags[0]['help'] ?? null; unset($tags[0]); $lazyCommandMap[$commandName] = $id; @@ -91,6 +94,7 @@ public function process(ContainerBuilder $container): void } $description ??= $tag['description'] ?? null; + $help ??= $tag['help'] ?? null; } $definition->addMethodCall('setName', [$commandName]); @@ -103,16 +107,12 @@ public function process(ContainerBuilder $container): void $definition->addMethodCall('setHidden', [true]); } - if (!$description) { - if (!$r = $container->getReflectionClass($class)) { - throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); - } - if (!$r->isSubclassOf(Command::class)) { - throw new InvalidArgumentException(\sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class)); - } - $description = str_replace('%', '%%', $class::getDefaultDescription() ?? ''); + if ($help && $invokableRef) { + $definition->addMethodCall('setHelp', [str_replace('%', '%%', $help)]); } + $description ??= str_replace('%', '%%', $class::getDefaultDescription() ?? ''); + if ($description) { $definition->addMethodCall('setDescription', [$description]); diff --git a/src/Symfony/Component/Console/Tests/Command/CommandTest.php b/src/Symfony/Component/Console/Tests/Command/CommandTest.php index 199c0c3096c8d..ef6f04c2d922f 100644 --- a/src/Symfony/Component/Console/Tests/Command/CommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/CommandTest.php @@ -434,6 +434,7 @@ public function testCommandAttribute() $this->assertSame('foo', $command->getName()); $this->assertSame('desc', $command->getDescription()); + $this->assertSame('help', $command->getHelp()); $this->assertTrue($command->isHidden()); $this->assertSame(['f'], $command->getAliases()); } @@ -473,7 +474,7 @@ function createClosure() }; } -#[AsCommand(name: 'foo', description: 'desc', hidden: true, aliases: ['f'])] +#[AsCommand(name: 'foo', description: 'desc', hidden: true, aliases: ['f'], help: 'help')] class Php8Command extends Command { } diff --git a/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php b/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php index 0df863720524a..8a0c1e6b2bbf5 100644 --- a/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php +++ b/src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php @@ -176,6 +176,7 @@ public function testEscapesDefaultFromPhp() $this->assertSame('%cmd%', $command->getName()); $this->assertSame(['%cmdalias%'], $command->getAliases()); $this->assertSame('Creates a 80% discount', $command->getDescription()); + $this->assertSame('The %command.name% help content.', $command->getHelp()); } public function testProcessThrowAnExceptionIfTheServiceIsAbstract() @@ -310,12 +311,19 @@ public function testProcessInvokableCommand() $container->addCompilerPass(new AddConsoleCommandPass(), PassConfig::TYPE_BEFORE_REMOVING); $definition = new Definition(InvokableCommand::class); - $definition->addTag('console.command', ['command' => 'invokable', 'description' => 'Just testing']); + $definition->addTag('console.command', [ + 'command' => 'invokable', + 'description' => 'The command description', + 'help' => 'The %command.name% command help content.', + ]); $container->setDefinition('invokable_command', $definition); $container->compile(); + $command = $container->get('console.command_loader')->get('invokable'); self::assertTrue($container->has('invokable_command.command')); + self::assertSame('The command description', $command->getDescription()); + self::assertSame('The %command.name% command help content.', $command->getHelp()); } } @@ -328,7 +336,7 @@ class NamedCommand extends Command { } -#[AsCommand(name: '%cmd%|%cmdalias%', description: 'Creates a 80% discount')] +#[AsCommand(name: '%cmd%|%cmdalias%', description: 'Creates a 80% discount', help: 'The %command.name% help content.')] class EscapedDefaultsFromPhpCommand extends Command { } @@ -346,7 +354,7 @@ public function __construct() } } -#[AsCommand(name: 'invokable', description: 'Just testing')] +#[AsCommand(name: 'invokable', description: 'Just testing', help: 'The %command.name% help content.')] class InvokableCommand { public function __invoke(): void