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));