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