Skip to content

Commit a7024e6

Browse files
committed
Command simplification and deprecations
1 parent e348b70 commit a7024e6

File tree

22 files changed

+231
-112
lines changed

22 files changed

+231
-112
lines changed

UPGRADE-7.3.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,38 @@ Console
3030
});
3131
```
3232

33+
* Configuring the command name via `Command::__construct()` is deprecated. Alternatives include
34+
using `setName()` method or specifying it with `AsCommand` attribute
35+
36+
*Before*
37+
```php
38+
$command = new Command('foo');
39+
```
40+
```php
41+
class BarCommand extends Command
42+
{
43+
public function __construct()
44+
{
45+
parent::__construct('bar');
46+
}
47+
}
48+
```
49+
50+
*After*
51+
```php
52+
$command = new Command();
53+
$command->setName('foobar');
54+
```
55+
```php
56+
#[AsCommand(name: 'bar')]
57+
class BarCommand extends Command
58+
{
59+
}
60+
```
61+
62+
* Static methods `Command::getDefaultName()` and `Command::getDefaultDescription()` are deprecated.
63+
Extract the command name and description through class reflection instead
64+
3365
FrameworkBundle
3466
---------------
3567

src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,15 @@ public function testLogsFromListeners()
176176
$logger->info('After terminate message.');
177177
});
178178

179-
$event = new ConsoleCommandEvent(new Command('foo'), $this->createMock(InputInterface::class), $output);
179+
$command = new Command();
180+
$command->setName('foo');
181+
182+
$event = new ConsoleCommandEvent($command, $this->createMock(InputInterface::class), $output);
180183
$dispatcher->dispatch($event, ConsoleEvents::COMMAND);
181184
$this->assertStringContainsString('Before command message.', $out = $output->fetch());
182185
$this->assertStringContainsString('After command message.', $out);
183186

184-
$event = new ConsoleTerminateEvent(new Command('foo'), $this->createMock(InputInterface::class), $output, 0);
187+
$event = new ConsoleTerminateEvent($command, $this->createMock(InputInterface::class), $output, 0);
185188
$dispatcher->dispatch($event, ConsoleEvents::TERMINATE);
186189
$this->assertStringContainsString('Before terminate message.', $out = $output->fetch());
187190
$this->assertStringContainsString('After terminate message.', $out);

src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ public function testBundleCommandsAreRetrievable()
7171

7272
public function testBundleSingleCommandIsRetrievable()
7373
{
74-
$command = new Command('example');
74+
$command = new Command();
75+
$command->setName('example');
7576

7677
$bundle = $this->createBundleMock([$command]);
7778

@@ -84,7 +85,8 @@ public function testBundleSingleCommandIsRetrievable()
8485

8586
public function testBundleCommandCanBeFound()
8687
{
87-
$command = new Command('example');
88+
$command = new Command();
89+
$command->setName('example');
8890

8991
$bundle = $this->createBundleMock([$command]);
9092

@@ -97,7 +99,8 @@ public function testBundleCommandCanBeFound()
9799

98100
public function testBundleCommandCanBeFoundByAlias()
99101
{
100-
$command = new Command('example');
102+
$command = new Command();
103+
$command->setName('example');
101104
$command->setAliases(['alias']);
102105

103106
$bundle = $this->createBundleMock([$command]);
@@ -111,14 +114,16 @@ public function testBundleCommandCanBeFoundByAlias()
111114

112115
public function testBundleCommandCanOverriddeAPreExistingCommandWithTheSameName()
113116
{
114-
$command = new Command('example');
117+
$command = new Command();
118+
$command->setName('example');
115119

116120
$bundle = $this->createBundleMock([$command]);
117121

118122
$kernel = $this->getKernel([$bundle]);
119123

120124
$application = new Application($kernel);
121-
$newCommand = new Command('example');
125+
$newCommand = new Command();
126+
$newCommand->setName('example');
122127
$application->add($newCommand);
123128

124129
$this->assertSame($newCommand, $application->get('example'));
@@ -135,7 +140,7 @@ public function testRunOnlyWarnsOnUnregistrableCommand()
135140
$kernel
136141
->method('getBundles')
137142
->willReturn([$this->createBundleMock(
138-
[(new Command('fine'))->setCode(function (InputInterface $input, OutputInterface $output) { $output->write('fine'); })]
143+
[(new Command())->setName('fine')->setCode(function (InputInterface $input, OutputInterface $output) { $output->write('fine'); })]
139144
)]);
140145
$kernel
141146
->method('getContainer')
@@ -193,7 +198,7 @@ public function testRunOnlyWarnsOnUnregistrableCommandAtTheEnd()
193198
$kernel
194199
->method('getBundles')
195200
->willReturn([$this->createBundleMock(
196-
[(new Command('fine'))->setCode(function (InputInterface $input, OutputInterface $output) { $output->write('fine'); })]
201+
[(new Command())->setName('fine')->setCode(function (InputInterface $input, OutputInterface $output) { $output->write('fine'); })]
197202
)]);
198203
$kernel
199204
->method('getContainer')

src/Symfony/Component/Console/Application.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ public function getLongVersion(): string
513513
*/
514514
public function register(string $name): Command
515515
{
516-
return $this->add(new Command($name));
516+
return $this->add((new Command())->setName($name));
517517
}
518518

519519
/**

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ CHANGELOG
77
* Add support for invokable commands and add `#[Argument]` and `#[Option]` attributes to define input arguments and options
88
* Deprecate not declaring the parameter type in callable commands defined through `setCode` method
99
* Add support for help definition via `AsCommand` attribute
10+
* Delay command initialization and configuration
11+
* Deprecate configuring the Command name via `Command::__construct()`
12+
* Deprecate static methods `Command::getDefaultName()` and `Command::getDefaultDescription()`
1013

1114
7.2
1215
---

src/Symfony/Component/Console/Command/Command.php

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,29 @@ class Command
5353
private array $synopsis = [];
5454
private array $usages = [];
5555
private ?HelperSet $helperSet = null;
56+
private bool $initialized = false;
5657

58+
/**
59+
* @deprecated since Symfony 7.3
60+
*/
5761
public static function getDefaultName(): ?string
5862
{
63+
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);
64+
5965
if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) {
6066
return $attribute[0]->newInstance()->name;
6167
}
6268

6369
return null;
6470
}
6571

72+
/**
73+
* @deprecated since Symfony 7.3
74+
*/
6675
public static function getDefaultDescription(): ?string
6776
{
77+
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);
78+
6879
if ($attribute = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) {
6980
return $attribute[0]->newInstance()->description;
7081
}
@@ -79,36 +90,11 @@ public static function getDefaultDescription(): ?string
7990
*/
8091
public function __construct(?string $name = null)
8192
{
82-
$this->definition = new InputDefinition();
83-
84-
if (null === $name && null !== $name = static::getDefaultName()) {
85-
$aliases = explode('|', $name);
86-
87-
if ('' === $name = array_shift($aliases)) {
88-
$this->setHidden(true);
89-
$name = array_shift($aliases);
90-
}
91-
92-
$this->setAliases($aliases);
93+
if ($name) {
94+
trigger_deprecation('symfony/console', '7.3', 'The "$name" argument of "%s()" is deprecated, use "%s::setName()" or "%s" attribute instead.', __METHOD__, __CLASS__, AsCommand::class);
9395
}
9496

95-
if (null !== $name) {
96-
$this->setName($name);
97-
}
98-
99-
if ('' === $this->description) {
100-
$this->setDescription(static::getDefaultDescription() ?? '');
101-
}
102-
103-
if ('' === $this->help && $attributes = (new \ReflectionClass(static::class))->getAttributes(AsCommand::class)) {
104-
$this->setHelp($attributes[0]->newInstance()->help ?? '');
105-
}
106-
107-
if (\is_callable($this)) {
108-
$this->code = new InvokableCommand($this, $this(...));
109-
}
110-
111-
$this->configure();
97+
$this->init($name);
11298
}
11399

114100
/**
@@ -333,6 +319,8 @@ public function setCode(callable $code): static
333319
*/
334320
public function mergeApplicationDefinition(bool $mergeArgs = true): void
335321
{
322+
$this->init();
323+
336324
if (null === $this->application) {
337325
return;
338326
}
@@ -356,6 +344,8 @@ public function mergeApplicationDefinition(bool $mergeArgs = true): void
356344
*/
357345
public function setDefinition(array|InputDefinition $definition): static
358346
{
347+
$this->init();
348+
359349
if ($definition instanceof InputDefinition) {
360350
$this->definition = $definition;
361351
} else {
@@ -385,6 +375,8 @@ public function getDefinition(): InputDefinition
385375
*/
386376
public function getNativeDefinition(): InputDefinition
387377
{
378+
$this->init();
379+
388380
$definition = $this->definition ?? throw new LogicException(\sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class));
389381

390382
if ($this->code && !$definition->getArguments() && !$definition->getOptions()) {
@@ -407,6 +399,8 @@ public function getNativeDefinition(): InputDefinition
407399
*/
408400
public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
409401
{
402+
$this->init();
403+
410404
$this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
411405
$this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues));
412406

@@ -427,6 +421,8 @@ public function addArgument(string $name, ?int $mode = null, string $description
427421
*/
428422
public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static
429423
{
424+
$this->init();
425+
430426
$this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
431427
$this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues));
432428

@@ -474,6 +470,8 @@ public function setProcessTitle(string $title): static
474470
*/
475471
public function getName(): ?string
476472
{
473+
$this->init();
474+
477475
return $this->name;
478476
}
479477

@@ -494,6 +492,8 @@ public function setHidden(bool $hidden = true): static
494492
*/
495493
public function isHidden(): bool
496494
{
495+
$this->init();
496+
497497
return $this->hidden;
498498
}
499499

@@ -514,6 +514,8 @@ public function setDescription(string $description): static
514514
*/
515515
public function getDescription(): string
516516
{
517+
$this->init();
518+
517519
return $this->description;
518520
}
519521

@@ -534,6 +536,8 @@ public function setHelp(string $help): static
534536
*/
535537
public function getHelp(): string
536538
{
539+
$this->init();
540+
537541
return $this->help;
538542
}
539543

@@ -586,6 +590,8 @@ public function setAliases(iterable $aliases): static
586590
*/
587591
public function getAliases(): array
588592
{
593+
$this->init();
594+
589595
return $this->aliases;
590596
}
591597

@@ -596,6 +602,8 @@ public function getAliases(): array
596602
*/
597603
public function getSynopsis(bool $short = false): string
598604
{
605+
$this->init();
606+
599607
$key = $short ? 'short' : 'long';
600608

601609
if (!isset($this->synopsis[$key])) {
@@ -644,6 +652,48 @@ public function getHelper(string $name): HelperInterface
644652
return $this->helperSet->get($name);
645653
}
646654

655+
private function init(?string $name = null): void
656+
{
657+
if ($this->initialized) {
658+
return;
659+
}
660+
661+
$this->definition = new InputDefinition();
662+
663+
$attribute = ((new \ReflectionClass(static::class))->getAttributes(AsCommand::class)[0] ?? null)?->newInstance();
664+
665+
if (null === $name && null !== $name = $attribute?->name) {
666+
$aliases = explode('|', $name);
667+
668+
if ('' === $name = array_shift($aliases)) {
669+
$this->setHidden(true);
670+
$name = array_shift($aliases);
671+
}
672+
673+
$this->setAliases($aliases);
674+
}
675+
676+
if (null !== $name) {
677+
$this->setName($name);
678+
}
679+
680+
if ('' === $this->description && $attribute?->description) {
681+
$this->setDescription($attribute->description);
682+
}
683+
684+
if ('' === $this->help && $attribute?->help) {
685+
$this->setHelp($attribute->help);
686+
}
687+
688+
if (\is_callable($this)) {
689+
$this->code = new InvokableCommand($this, $this(...));
690+
}
691+
692+
$this->initialized = true;
693+
694+
$this->configure();
695+
}
696+
647697
/**
648698
* Validates a command name.
649699
*

src/Symfony/Component/Console/DependencyInjection/AddConsoleCommandPass.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Console\DependencyInjection;
1313

14+
use Symfony\Component\Console\Attribute\AsCommand;
1415
use Symfony\Component\Console\Command\Command;
1516
use Symfony\Component\Console\Command\LazyCommand;
1617
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
@@ -57,7 +58,10 @@ public function process(ContainerBuilder $container): void
5758
$invokableRef = null;
5859
}
5960

60-
$aliases = $tags[0]['command'] ?? str_replace('%', '%%', $class::getDefaultName() ?? '');
61+
/** @var AsCommand|null $attribute */
62+
$attribute = ($r->getAttributes(AsCommand::class)[0] ?? null)?->newInstance();
63+
64+
$aliases = str_replace('%', '%%', $tags[0]['command'] ?? $attribute?->name ?? '');
6165
$aliases = explode('|', $aliases);
6266
$commandName = array_shift($aliases);
6367

@@ -111,10 +115,10 @@ public function process(ContainerBuilder $container): void
111115
$definition->addMethodCall('setHelp', [str_replace('%', '%%', $help)]);
112116
}
113117

114-
$description ??= str_replace('%', '%%', $class::getDefaultDescription() ?? '');
118+
$description ??= $attribute?->description ?? '';
115119

116120
if ($description) {
117-
$definition->addMethodCall('setDescription', [$description]);
121+
$definition->addMethodCall('setDescription', [str_replace('%', '%%', $description)]);
118122

119123
$container->register('.'.$id.'.lazy', LazyCommand::class)
120124
->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]);

0 commit comments

Comments
 (0)