8000 [Console] enable describing commands in ways that make the `list` com… · symfony/symfony@a340975 · GitHub
[go: up one dir, main page]

Skip to content

Commit a340975

Browse files
[Console] enable describing commands in ways that make the list command lazy
1 parent 28533aa commit a340975

File tree

5 files changed

+320
-6
lines changed

5 files changed

+320
-6
lines changed

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ CHANGELOG
44
5.3
55
---
66

7-
* Added `GithubActionReporter` to render annotations in a Github Action
8-
* Added `InputOption::VALUE_NEGATABLE` flag to handle `--foo`/`--no-foo` options.
7+
* Add `GithubActionReporter` to render annotations in a Github Action
8+
* Add `InputOption::VALUE_NEGATABLE` flag to handle `--foo`/`--no-foo` options
9+
* Add the `Command::$defaultDescription` static property and the `description` attribute
10+
on the `console.command` tag to allow the `list` command to instantiate commands lazily
911

1012
5.2.0
1113
-----

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ class Command
3939
*/
4040
protected static $defaultName;
4141

42+
/**
43+
* @var string|null The default command description
44+
*/
45+
protected static $defaultDescription;
46+
4247
private $application;
4348
private $name;
4449
private $processTitle;
@@ -65,6 +70,17 @@ public static function getDefaultName()
6570
return $class === $r->class ? static::$defaultName : null;
6671
}
6772

73+
/**
74+
* @return string|null The default command description or null when no default description is set
75+
*/
76+
public static function getDefaultDescription(): ?string
77+
{
78+
$class = static::class;
79+
$r = new \ReflectionProperty($class, 'defaultDescription');
80+
81+
return $class === $r->class ? static::$defaultDescription : null;
82+
}
83+
6884
/**
6985
* @param string|null $name The name of the command; passing null means it must be set in configure()
7086
*
@@ -298,6 +314,8 @@ public function setCode(callable $code)
298314
* This method is not part of public API and should not be used directly.
299315
*
300316
* @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments
317+
*
318+
* @internal
301319
*/
302320
public function mergeApplicationDefinition(bool $mergeArgs = true)
303321
{
@@ -554,11 +572,14 @@ public function getProcessedHelp()
554572
*/
555573
public function setAliases(iterable $aliases)
556574
{
575+
$list = [];
576+
557577
foreach ($aliases as $alias) {
558578
$this->validateName($alias);
579+
$list[] = $alias;
559580
}
560581

561-
$this->aliases = $aliases;
582+
$this->aliases = \is_array($aliases) ? $aliases : $list;
562583

563584
return $this;
564585
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Command;
13+
14+
use Symfony\Component\Console\Application;
15+
use Symfony\Component\Console\Helper\HelperSet;
16+
use Symfony\Component\Console\Input\InputDefinition;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Output\OutputInterface;
19+
20+
/**
21+
* @author Nicolas Grekas <p@tchwork.com>
22+
*/
23+
final class LazyCommand extends Command
24+
{
25+
private $command;
26+
private $isEnabled;
27+
28+
public function __construct(string $name, array $aliases, string $description, bool $isHidden, \Closure $commandFactory, ?bool $isEnabled = true)
29+
{
30+
$this->setName($name);
31+
$this->setAliases($aliases);
32+
$this->setHidden($isHidden);
33+
$this->setDescription($description);
34+
$this->command = $commandFactory;
35+
$this->isEnabled = $isEnabled;
36+
}
37+
38+
public function ignoreValidationErrors(): void
39+
{
40+
$this->getCommand()->ignoreValidationErrors();
41+
}
42+
43+
public function setApplication(Application $application = null): void
44+
{
45+
if ($this->command instanceof parent) {
46+
$this->command->setApplication($application);
47+
}
48+
49+
parent::setApplication($application);
50+
}
51+
52+
public function setHelperSet(HelperSet $helperSet): void
53+
{
54+
if ($this->command instanceof parent) {
55+
$this->command->setHelperSet($helperSet);
56+
}
57+
58+
parent::setHelperSet($helperSet);
59+
}
60+
61+
public function isEnabled(): bool
62+
{
63+
return $this->isEnabled ?? $this->getCommand()->isEnabled();
64+
}
65+
66+
public function run(InputInterface $input, OutputInterface $output): int
67+
{
68+
return $this->getCommand()->run($input, $output);
69+
}
70+
71+
/**
72+
* @return $this
73+
*/
74+
public function setCode(callable $code): self
75+
{
76+
$this->getCommand()->setCode($code);
77+
78+
return $this;
79+
}
80+
81+
/**
82+
* @internal
83+
*/
84+
public function mergeApplicationDefinition(bool $mergeArgs = true): void
85+
{
86+
$this->getCommand()->mergeApplicationDefinition($mergeArgs);
87+
}
88+
89+
/**
90+
* @return $this
91+
*/
92+
public function setDefinition($definition): self
93+
{
94+
$this->getCommand()->setDefinition($definition);
95+
96+
return $this;
97+
}
98+
99+
public function getDefinition(): InputDefinition
100+
{
101+
return $this->getCommand()->getDefinition();
102+
}
103+
104+
public function getNativeDefinition(): InputDefinition
105+
{
106+
return $this->getCommand()->getNativeDefinition();
107+
}
108+
109+
/**
110+
* @return $this
111+
*/
112+
public function addArgument(string $name, int $mode = null, string $description = '', $default = null): self
113+
{
114+
$this->getCommand()->addArgument($name, $mode, $description, $default);
115+
116+
return $this;
117+
}
118+
119+
/**
120+
* @return $this
121+
*/
122+
public function addOption(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null): self
123+
{
124+
$this->getCommand()->addOption($name, $shortcut, $mode, $description, $default);
125+
126+
return $this;
127+
}
128+
129+
/**
130+
* @return $this
131+
*/
132+
public function setProcessTitle(string $title): self
133+
{
134+
$this->getCommand()->setProcessTitle($title);
135+
136+
return $this;
137+
}
138+
139+
/**
140+
* @return $this
141+
*/
142+
public function setHelp(string $help): self
143+
{
144+
$this->getCommand()->setHelp($help);
145+
146+
return $this;
147+
}
148+
149+
public function getHelp(): string
150+
{
151+
return $this->getCommand()->getHelp();
152+
}
153+
154+
public function getProcessedHelp(): string
155+
{
156+
return $this->getCommand()->getProcessedHelp();
157+
}
158+
159+
public function getSynopsis(bool $short = false): string
160+
{
161+
return $this->getCommand()->getSynopsis($short);
162+
}
163+
164+
/**
165+
* @return $this
166+
*/
167+
public function addUsage(string $usage): self
168+
{
169+
$this->getCommand()->addUsage($usage);
170+
171+
return $this;
172+
}
173+
174+
public function getUsages(): array
175+
{
176+
return $this->getCommand()->getUsages();
177+
}
178+
179+
/**
180+
* @return mixed
181+
*/
182+
public function getHelper(string $name)
183+
{
184+
return $this->getCommand()->getHelper($name);
185+
}
186+
187+
public function getCommand(): parent
188+
{
189+
if (!$this->command instanceof \Closure) {
190+
return $this->command;
191+
}
192+
193+
$command = $this->command = ($this->command)();
194+
$command->setApplication($this->getApplication());
195+
196+
if (null !== $this->getHelperSet()) {
197+
$command->setHelperSet($this->getHelperSet());
198+
}
199+
200+
$command->setName($this->getName());
201+
$command->setAliases($this->getAliases());
202+
$command->setHidden($this->isHidden());
203+
$command->setDescription($this->getDescription());
204+
205+
return $command;
206+
}
207+
}

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
namespace Symfony\Component\Console\DependencyInjection;
1313

1414
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Command\LazyCommand;
1516
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
17+
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
1618
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
1719
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
1820
use Symfony\Component\DependencyInjection\ContainerBuilder;
1921
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
22+
use Symfony\Component\DependencyInjection\Reference;
2023
use Symfony\Component\DependencyInjection\TypedReference;
2124

2225
/**
@@ -52,15 +55,22 @@ public function process(ContainerBuilder $container)
5255
$class = $container->getParameterBag()->resolveValue($definition->getClass());
5356

5457
if (isset($tags[0]['command'])) {
55-
$commandName = $tags[0]['command'];
58+
$aliases = $tags[0]['command'];
5659
} else {
5760
if (!$r = $container->getReflectionClass($class)) {
5861
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
5962
}
6063
if (!$r->isSubclassOf(Command::class)) {
6164
throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class));
6265
}
63-
$commandName = $class::getDefaultName();
66+
$aliases = $class::getDefaultName();
67+
}
68+
69+
$aliases = explode('|', $aliases);
70+
$commandName = array_shift($aliases);
71+
72+
if ($isHidden = '' === $commandName) {
73+
$commandName = array_shift($aliases);
6474
}
6575

6676
if (null === $commandName) {
@@ -74,23 +84,49 @@ public function process(ContainerBuilder $container)
7484
continue;
7585
}
7686

87+
$description = $tags[0]['description'] ?? null;
88+
7789
unset($tags[0]);
7890
$lazyCommandMap[$commandName] = $id;
7991
$lazyCommandRefs[$id] = new TypedReference($id, $class);
80-
$aliases = [];
8192

8293
foreach ($tags as $tag) {
8394
if (isset($tag['command'])) {
8495
$aliases[] = $tag['command'];
8596
$lazyCommandMap[$tag['command']] = $id;
8697
}
98+
99+
$description = $description ?? $tag['description'] ?? null;
87100
}
88101

89102
$definition->addMethodCall('setName', [$commandName]);
90103

91104
if ($aliases) {
92105
$definition->addMethodCall('setAliases', [$aliases]);
93106
}
107+
108+
if ($isHidden) {
109+
$definition->addMethodCall('setHidden', [true]);
110+
}
111+
112+
if (!$description) {
113+
if (!$r = $container->getReflectionClass($class)) {
114+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
115+
}
116+
if (!$r->isSubclassOf(Command::class)) {
117+
throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, $this->commandTag, Command::class));
118+
}
119+
$description = $class::getDefaultDescription();
120+
}
121+
122+
if ($description) {
123+
$definition->addMethodCall('setDescription', [$description]);
124+
125+
$container->register('.'.$id.'.lazy', LazyCommand::class)
126+
->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]);
127+
128+
$lazyCommandRefs[$id] = new Reference('.'.$id.'.lazy');
129+
}
94130
}
95131

96132
$container

0 commit comments

Comments
 (0)
0