From f61c319ed2edc9805d0542836aadccbddeee3f68 Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Mon, 20 Jan 2025 10:04:40 -0500 Subject: [PATCH] Command simplification and deprecations --- UPGRADE-7.3.md | 3 + src/Symfony/Component/Console/CHANGELOG.md | 2 + .../Component/Console/Command/Command.php | 106 +++++++++++++----- .../AddConsoleCommandPass.php | 10 +- .../Console/Tests/ApplicationTest.php | 6 +- .../Console/Tests/Command/CommandTest.php | 28 +++-- 6 files changed, 112 insertions(+), 43 deletions(-) diff --git a/UPGRADE-7.3.md b/UPGRADE-7.3.md index 61f69a3acf377..6a5cac46b9afc 100644 --- a/UPGRADE-7.3.md +++ b/UPGRADE-7.3.md @@ -30,6 +30,9 @@ Console }); ``` + * Static methods `Command::getDefaultName()` and `Command::getDefaultDescription()` are deprecated. + Extract the command name and description through class reflection instead + FrameworkBundle --------------- diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 77b109b812410..12838589e6a72 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -7,6 +7,8 @@ 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 + * Delay command initialization and configuration + * Deprecate static methods `Command::getDefaultName()` and `Command::getDefaultDescription()` 7.2 --- diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index fb410d7f8adea..41e46464b068b 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -53,9 +53,15 @@ class Command private array $synopsis = []; private array $usages = []; private ?HelperSet $helperSet = null; + private bool $initialized = false; + /** + * @deprecated since Symfony 7.3 + */ public static function getDefaultName(): ?string { + trigger_deprecation('symfony/console', '7.3', 'The static method "%s()" is deprecated and will be removed in Symfony 8.0, extract the command name from the "%s" attribute instead.', __METHOD__, AsCommand::class); + if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) { return $attribute[0]->newInstance()->name; } @@ -63,8 +69,13 @@ public static function getDefaultName(): ?string return null; } + /** + * @deprecated since Symfony 7.3 + */ public static function getDefaultDescription(): ?string { + trigger_deprecation('symfony/console', '7.3', 'The static method "%s()" is deprecated and will be removed in Symfony 8.0, extract the command description from the "%s" attribute instead.', __METHOD__, AsCommand::class); + if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) { return $attribute[0]->newInstance()->description; } @@ -79,36 +90,7 @@ public static function getDefaultDescription(): ?string */ public function __construct(?string $name = null) { - $this->definition = new InputDefinition(); - - if (null === $name && null !== $name = static::getDefaultName()) { - $aliases = explode('|', $name); - - if ('' === $name = array_shift($aliases)) { - $this->setHidden(true); - $name = array_shift($aliases); - } - - $this->setAliases($aliases); - } - - if (null !== $name) { - $this->setName($name); - } - - if ('' === $this->description) { - $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(...)); - } - - $this->configure(); + $this->init($name); } /** @@ -333,6 +315,8 @@ public function setCode(callable $code): static */ public function mergeApplicationDefinition(bool $mergeArgs = true): void { + $this->init(); + if (null === $this->application) { return; } @@ -356,6 +340,8 @@ public function mergeApplicationDefinition(bool $mergeArgs = true): void */ public function setDefinition(array|InputDefinition $definition): static { + $this->init(); + if ($definition instanceof InputDefinition) { $this->definition = $definition; } else { @@ -385,6 +371,8 @@ public function getDefinition(): InputDefinition */ public function getNativeDefinition(): InputDefinition { + $this->init(); + $definition = $this->definition ?? throw new LogicException(\sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class)); if ($this->code && !$definition->getArguments() && !$definition->getOptions()) { @@ -407,6 +395,8 @@ public function getNativeDefinition(): InputDefinition */ public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static { + $this->init(); + $this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues)); $this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues)); @@ -427,6 +417,8 @@ public function addArgument(string $name, ?int $mode = null, string $description */ public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static { + $this->init(); + $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues)); $this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues)); @@ -474,6 +466,8 @@ public function setProcessTitle(string $title): static */ public function getName(): ?string { + $this->init(); + return $this->name; } @@ -494,6 +488,8 @@ public function setHidden(bool $hidden = true): static */ public function isHidden(): bool { + $this->init(); + return $this->hidden; } @@ -514,6 +510,8 @@ public function setDescription(string $description): static */ public function getDescription(): string { + $this->init(); + return $this->description; } @@ -534,6 +532,8 @@ public function setHelp(string $help): static */ public function getHelp(): string { + $this->init(); + return $this->help; } @@ -586,6 +586,8 @@ public function setAliases(iterable $aliases): static */ public function getAliases(): array { + $this->init(); + return $this->aliases; } @@ -596,6 +598,8 @@ public function getAliases(): array */ public function getSynopsis(bool $short = false): string { + $this->init(); + $key = $short ? 'short' : 'long'; if (!isset($this->synopsis[$key])) { @@ -644,6 +648,48 @@ public function getHelper(string $name): HelperInterface return $this->helperSet->get($name); } + private function init(?string $name = null): void + { + if ($this->initialized) { + return; + } + + $this->definition = new InputDefinition(); + + $attribute = ((new \ReflectionClass(static::class))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance(); + + if (null === $name && null !== $name = $attribute?->name) { + $aliases = explode('|', $name); + + if ('' === $name = array_shift($aliases)) { + $this->setHidden(true); + $name = array_shift($aliases); + } + + $this->setAliases($aliases); + } + + if (null !== $name) { + $this->setName($name); + } + + if ('' === $this->description && $attribute?->description) { + $this->setDescription($attribute->description); + } + + if ('' === $this->help && $attribute?->help) { + $this->setHelp($attribute->help); + } + + if (\is_callable($this)) { + $this->code = new InvokableCommand($this, $this(...)); + } + + $this->initialized = true; + + $this->configure(); + } + /** * Validates a command name. * diff --git a/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php index 248ad3276a130..dd1f441c2fe24 100644 --- a/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php +++ b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Console\DependencyInjection; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\LazyCommand; use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; @@ -57,7 +58,10 @@ public function process(ContainerBuilder $container): void $invokableRef = null; } - $aliases = $tags[0]['command'] ?? str_replace('%', '%%', $class::getDefaultName() ?? ''); + /** @var AsCommand|null $attribute */ + $attribute = ($r->getAttributes(AsCommand::class)[0] ?? null)?->newInstance(); + + $aliases = str_replace('%', '%%', $tags[0]['command'] ?? $attribute?->name ?? ''); $aliases = explode('|', $aliases); $commandName = array_shift($aliases); @@ -111,10 +115,10 @@ public function process(ContainerBuilder $container): void $definition->addMethodCall('setHelp', [str_replace('%', '%%', $help)]); } - $description ??= str_replace('%', '%%', $class::getDefaultDescription() ?? ''); + $description ??= $attribute?->description ?? ''; if ($description) { - $definition->addMethodCall('setDescription', [$description]); + $definition->addMethodCall('setDescription', [str_replace('%', '%%', $description)]); $container->register('.'.$id.'.lazy', LazyCommand::class) ->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]); diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 4f6e6cb96cf32..09d4fefca14b0 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -232,7 +232,7 @@ public function testAdd() public function testAddCommandWithEmptyConstructor() { $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Command class "Foo5Command" is not correctly initialized. You probably forgot to call the parent constructor.'); + $this->expectExceptionMessage('The command defined in "Foo5Command" cannot have an empty name.'); (new Application())->add(new \Foo5Command()); } @@ -2404,7 +2404,9 @@ private function createSignalableApplication(Command $command, ?EventDispatcherI if ($dispatcher) { $application->setDispatcher($dispatcher); } - $application->add(new LazyCommand($command::getDefaultName(), [], '', false, fn () => $command, true)); + /** @var AsCommand $attribute */ + $attribute = ((new \ReflectionClass($command))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance(); + $application->add(new LazyCommand($attribute->name, [], '', false, fn () => $command, true)); return $application; } diff --git a/src/Symfony/Component/Console/Tests/Command/CommandTest.php b/src/Symfony/Component/Console/Tests/Command/CommandTest.php index ef6f04c2d922f..942b509ed5a3e 100644 --- a/src/Symfony/Component/Console/Tests/Command/CommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/CommandTest.php @@ -427,9 +427,6 @@ public function testSetCodeWithStaticAnonymousFunction() public function testCommandAttribute() { - $this->assertSame('|foo|f', Php8Command::getDefaultName()); - $this->assertSame('desc', Php8Command::getDefaultDescription()); - $command = new Php8Command(); $this->assertSame('foo', $command->getName()); @@ -439,26 +436,41 @@ public function testCommandAttribute() $this->assertSame(['f'], $command->getAliases()); } - public function testAttributeOverridesProperty() + /** + * @group legacy + */ + public function testCommandAttributeWithDeprecatedMethods() { - $this->assertSame('my:command', MyAnnotatedCommand::getDefaultName()); - $this->assertSame('This is a command I wrote all by myself', MyAnnotatedCommand::getDefaultDescription()); + $this->assertSame('|foo|f', Php8Command::getDefaultName()); + $this->assertSame('desc', Php8Command::getDefaultDescription()); + } + public function testAttributeOverridesProperty() + { $command = new MyAnnotatedCommand(); $this->assertSame('my:command', $command->getName()); $this->assertSame('This is a command I wrote all by myself', $command->getDescription()); } + /** + * @group legacy + */ + public function testAttributeOverridesPropertyWithDeprecatedMethods() + { + $this->assertSame('my:command', MyAnnotatedCommand::getDefaultName()); + $this->assertSame('This is a command I wrote all by myself', MyAnnotatedCommand::getDefaultDescription()); + } + public function testDefaultCommand() { $apl = new Application(); - $apl->setDefaultCommand(Php8Command::getDefaultName()); + $apl->setDefaultCommand('foo'); $property = new \ReflectionProperty($apl, 'defaultCommand'); $this->assertEquals('foo', $property->getValue($apl)); - $apl->setDefaultCommand(Php8Command2::getDefaultName()); + $apl->setDefaultCommand('foo2'); $property = new \ReflectionProperty($apl, 'defaultCommand'); $this->assertEquals('foo2', $property->getValue($apl)); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy