diff --git a/UPGRADE-7.3.md b/UPGRADE-7.3.md index 61f69a3acf377..03535a1250cb6 100644 --- a/UPGRADE-7.3.md +++ b/UPGRADE-7.3.md @@ -30,6 +30,8 @@ Console }); ``` + * Deprecate methods `Command::getDefaultName()` and `Command::getDefaultDescription()` in favor of the `#[AsCommand]` attribute + FrameworkBundle --------------- diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 77b109b812410..fc2b64bf156bb 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -7,6 +7,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 + * Deprecate methods `Command::getDefaultName()` and `Command::getDefaultDescription()` in favor of the `#[AsCommand]` attribute 7.2 --- diff --git a/src/Symfony/Component/Console/Command/Command.php b/src/Symfony/Component/Console/Command/Command.php index fb410d7f8adea..5e30eb5fa8390 100644 --- a/src/Symfony/Component/Console/Command/Command.php +++ b/src/Symfony/Component/Console/Command/Command.php @@ -54,8 +54,13 @@ class Command private array $usages = []; private ?HelperSet $helperSet = null; + /** + * @deprecated since Symfony 7.3, use the #[AsCommand] attribute instead + */ public static function getDefaultName(): ?string { + trigger_deprecation('symfony/console', '7.3', 'Method "%s()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', __METHOD__); + if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) { return $attribute[0]->newInstance()->name; } @@ -63,8 +68,13 @@ public static function getDefaultName(): ?string return null; } + /** + * @deprecated since Symfony 7.3, use the #[AsCommand] attribute instead + */ public static function getDefaultDescription(): ?string { + trigger_deprecation('symfony/console', '7.3', 'Method "%s()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', __METHOD__); + if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) { return $attribute[0]->newInstance()->description; } @@ -81,7 +91,19 @@ public function __construct(?string $name = null) { $this->definition = new InputDefinition(); - if (null === $name && null !== $name = static::getDefaultName()) { + $attribute = ((new \ReflectionClass(static::class))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance(); + + if (null === $name) { + if (self::class !== (new \ReflectionMethod($this, 'getDefaultName'))->class) { + trigger_deprecation('symfony/console', '7.3', 'Overriding "Command::getDefaultName()" in "%s" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', static::class); + + $defaultName = static::getDefaultName(); + } else { + $defaultName = $attribute?->name; + } + } + + if (null === $name && null !== $name = $defaultName) { $aliases = explode('|', $name); if ('' === $name = array_shift($aliases)) { @@ -97,11 +119,19 @@ public function __construct(?string $name = null) } if ('' === $this->description) { - $this->setDescription(static::getDefaultDescription() ?? ''); + if (self::class !== (new \ReflectionMethod($this, 'getDefaultDescription'))->class) { + trigger_deprecation('symfony/console', '7.3', 'Overriding "Command::getDefaultDescription()" in "%s" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', static::class); + + $defaultDescription = static::getDefaultDescription(); + } else { + $defaultDescription = $attribute?->description; + } + + $this->setDescription($defaultDescription ?? ''); } - if ('' === $this->help && $attributes = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) { - $this->setHelp($attributes[0]->newInstance()->help ?? ''); + if ('' === $this->help) { + $this->setHelp($attribute?->help ?? ''); } if (\is_callable($this)) { diff --git a/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php b/src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php index 248ad3276a130..a90fb8f04606e 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,18 @@ 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(); + + if (Command::class !== (new \ReflectionMethod($class, 'getDefaultName'))->class) { + trigger_deprecation('symfony/console', '7.3', 'Overriding "Command::getDefaultName()" in "%s" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', $class); + + $defaultName = $class::getDefaultName(); + } else { + $defaultName = $attribute?->name; + } + + $aliases = str_replace('%', '%%', $tags[0]['command'] ?? $defaultName ?? ''); $aliases = explode('|', $aliases); $commandName = array_shift($aliases); @@ -111,10 +123,18 @@ public function process(ContainerBuilder $container): void $definition->addMethodCall('setHelp', [str_replace('%', '%%', $help)]); } - $description ??= str_replace('%', '%%', $class::getDefaultDescription() ?? ''); + if (!$description) { + if (Command::class !== (new \ReflectionMethod($class, 'getDefaultDescription'))->class) { + trigger_deprecation('symfony/console', '7.3', 'Overriding "Command::getDefaultDescription()" in "%s" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.', $class); + + $description = $class::getDefaultDescription(); + } else { + $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..7549a1d8af5a0 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -2404,7 +2404,7 @@ private function createSignalableApplication(Command $command, ?EventDispatcherI if ($dispatcher) { $application->setDispatcher($dispatcher); } - $application->add(new LazyCommand($command::getDefaultName(), [], '', false, fn () => $command, true)); + $application->add(new LazyCommand($command->getName(), [], '', 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..e417b0656e9d9 100644 --- a/src/Symfony/Component/Console/Tests/Command/CommandTest.php +++ b/src/Symfony/Component/Console/Tests/Command/CommandTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Console\Tests\Command; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Console\Application; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -28,6 +29,8 @@ class CommandTest extends TestCase { + use ExpectDeprecationTrait; + protected static string $fixturesPath; public static function setUpBeforeClass(): void @@ -427,9 +430,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,30 +439,62 @@ 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->expectDeprecation('Since symfony/console 7.3: Method "Symfony\Component\Console\Command\Command::getDefaultName()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.'); + $this->expectDeprecation('Since symfony/console 7.3: Method "Symfony\Component\Console\Command\Command::getDefaultDescription()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.'); + $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->expectDeprecation('Since symfony/console 7.3: Method "Symfony\Component\Console\Command\Command::getDefaultName()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.'); + $this->expectDeprecation('Since symfony/console 7.3: Method "Symfony\Component\Console\Command\Command::getDefaultDescription()" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.'); + + $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)); } + + /** + * @group legacy + */ + public function testDeprecatedMethods() + { + $this->expectDeprecation('Since symfony/console 7.3: Overriding "Command::getDefaultName()" in "Symfony\Component\Console\Tests\Command\FooCommand" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.'); + $this->expectDeprecation('Since symfony/console 7.3: Overriding "Command::getDefaultDescription()" in "Symfony\Component\Console\Tests\Command\FooCommand" is deprecated and will be removed in Symfony 8.0, use the #[AsCommand] attribute instead.'); + + new FooCommand(); + } } // In order to get an unbound closure, we should create it outside a class @@ -491,3 +523,16 @@ class MyAnnotatedCommand extends Command protected static $defaultDescription = 'This description should be ignored.'; } + +class FooCommand extends Command +{ + public static function getDefaultName(): ?string + { + return 'foo'; + } + + public static function getDefaultDescription(): ?string + { + return 'foo description'; + } +}