8000 [FEATURE] Implement lazy console command list · TYPO3-CMS/core@25e9591 · GitHub
[go: up one dir, main page]

Skip to content

Commit 25e9591

Browse files
bnfhelhum
authored andcommitted
[FEATURE] Implement lazy console command list
Based on the configuration syntax of the Symfony Console feature symfony/symfony#39851 …but implemented differently, using a registry pattern rather then a lazy-object pattern (like symfony does). Main motiviation for the registry pattern is following: Symfony LazyCommand wrappers add quite some complexity only for the sake of the list command, we already got lazy commands (in terms of execution) as our CommandRegistry implements the ConfigurationLoaderInterface that has been introduced by 2017 to add support for lazy commands. Now, that means we already got a registry for lazy commands, so it is logical to add lazy description handling there as well. We want to assure that the command list will never instantiate any commands. This is in constrast to the Symfony core LazyCommand approach, where legacy commands, that do not provide a compile time description, would still be instantiated during console command list. Also commands that return false in `isEnabled()` are now listed. That means enabled state is only evaluated during runtime. Therefore the special `dumpautoload` command is transformed into a lowlevel command in order to be hidden dependending on being run in composer-mode or not. Releases: master Resolves: #93174 Change-Id: Ifa68404cc81c64a335be30f2263a7eb17de0624d Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67635 Tested-by: TYPO3com <noreply@typo3.com> Tested-by: core-ci <typo3@b13.com> Tested-by: Benni Mack <benni@typo3.org> Tested-by: Helmut Hummel <typo3@helhum.io> Reviewed-by: Benni Mack <benni@typo3.org> Reviewed-by: Helmut Hummel <typo3@helhum.io>
1 parent 3fd52ed commit 25e9591

File tree

13 files changed

+403
-40
lines changed

13 files changed

+403
-40
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the TYPO3 CMS project.
7+
*
8+
* It is free software; you can redistribute it and/or modify it under
9+
* the terms of the GNU General Public License, either version 2
10+
* of the License, or any later version.
11+
*
12+
* For the full copyright and license information, please read the
13+
* LICENSE.txt file that was distributed with this source code.
14+
*
15+
* The TYPO3 project - inspiring people to share!
16+
*/
17+
18+
namespace TYPO3\CMS\Core\Command\Descriptor;
19+
20+
use Symfony\Component\Console\Application;
21+
use Symfony\Component\Console\Descriptor\ApplicationDescription;
22+
use Symfony\Component\Console\Descriptor\TextDescriptor as SymfonyTextDescriptor;
23+
use Symfony\Component\Console\Helper\Helper;
24+
use Symfony\Component\Console\Input\InputDefinition;
25+
use TYPO3\CMS\Core\Console\CommandRegistry;
26+
27+
/**
28+
* Text descriptor.
29+
*
30+
* @internal
31+
*/
32+
class TextDescriptor extends SymfonyTextDescriptor
33+
{
34+
private CommandRegistry $commandRegistry;
35+
36+
public function __construct(CommandRegistry $commandRegistry)
37+
{
38+
$this->commandRegistry = $commandRegistry;
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
protected function describeApplication(Application $application, array $options = [])
45+
{
46+
$describedNamespace = $options['namespace'] ?? null;
47+
$rawOutput = $options['raw_text'] ?? false;
48+
49+
$commands = $this->commandRegistry->filter($describedNamespace);
50+
51+
if ($rawOutput) {
52+
$width = $this->getColumnWidth(['' => ['commands' => array_keys($commands)]]);
53+
54+
foreach ($commands as $command) {
55+
$this->write(sprintf("%-{$width}s %s\n", $command['name'], strip_tags($command['description'])), true);
56+
}
57+
return;
58+
}
59+
60+
$namespaces = $this->commandRegistry->getNamespaces();
61+
$help = $application->getHelp();
62+
if ($help !== '') {
63+
$this->write($help . "\n\n", true);
64+
}
65+
66+
$this->write("<comment>Usage:</comment>\n", true);
67+
$this->write(" command [options] [arguments]\n\n");
68+
69+
$this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()));
70+
71+
$this->write("\n\n");
72+
73+
if ($describedNamespace) {
74+
$this->write(sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), true);
75+
$namespace = $namespaces[$describedNamespace] ?? [];
76+
$width = $this->getColumnWidth(['' => $namespace]);
77+
$this->describeNamespace($namespace, $commands, $width);
78+
} else {
79+
$this->write('<comment>Available commands:</comment>', true);
80+
// calculate max. width based on available commands per namespace
81+
$width = $this->getColumnWidth($namespaces);
82+
foreach ($namespaces as $namespace) {
83+
if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) {
84+
$this->write("\n");
85+
$this->write(' <comment>' . $namespace['id'] . '</comment>', true);
86+
}
87+
$this->describeNamespace($namespace, $commands, $width);
88+
}
89+
}
90+
91+
$this->write("\n");
92+
}
93+
94+
private function describeNamespace(array $namespace, array $commands, int $width): void
95+
{
96+
foreach ($namespace['commands'] as $name) {
97+
$this->write("\n");
98+
$spacingWidth = $width - Helper::strlen($name);
99+
$command = $commands[$name];
100+
101+
$aliases = count($command['aliases']) ? '[' . implode('|', $command['aliases']) . '] ' : '';
102+
$this->write(sprintf(' <info>%s</info>%s%s', $name, str_repeat(' ', $spacingWidth), $aliases . $command['description']), true);
103+
}
104+
}
105+
106+
private function getColumnWidth(array $namespaces): int
107+
{
108+
$widths = [];
109+
foreach ($namespaces as $name => $namespace) {
110+
$widths[] = Helper::strlen($name);
111+
foreach ($namespace['commands'] as $commandName) {
112+
$widths[] = Helper::strlen($commandName);
113+
}
114+
}
115+
116+
return $widths ? max($widths) + 2 : 0;
117+
}
118+
}

Classes/Command/DumpAutoloadCommand.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class DumpAutoloadCommand extends Command
3434
*/
3535
protected function configure()
3636
{
37+
$this->setName('dumpautoload');
3738
$this->setDescription('Updates class loading information in non-composer mode.');
3839
$this->setHelp('This command is only needed during development. The extension manager takes care of creating or updating this info properly during extension (de-)activation.');
3940
$this->setAliases([

Classes/Command/ExtensionListCommand.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ public function __construct(PackageManager $packageManager)
5050
protected function configure()
5151
{
5252
$this
53-
->setDescription('Shows the list of extensions available to the system.')
5453
->addOption(
5554
'all',
5655
'a',

Classes/Command/ListCommand.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the TYPO3 CMS project.
7+
*
8+
* It is free software; you can redistribute it and/or modify it under
9+
* the terms of the GNU General Public License, either version 2
10+
* of the License, or any later version.
11+
*
12+
* For the full copyright and license information, please read the
13+
* LICENSE.txt file that was distributed with this source code.
14+
*
15+
* The TYPO3 project - inspiring people to share!
16+
*/
17+
18+
namespace TYPO3\CMS\Core\Command;
19+
20+
use Symfony\Component\Console\Command\ListCommand as SymfonyListCommand;
21+
use Symfony\Component\Console\Helper\DescriptorHelper;
22+
use Symfony\Component\Console\Input\InputInterface;
23+
use Symfony\Component\Console\Output\OutputInterface;
24+
use TYPO3\CMS\Core\Command\Descriptor\TextDescriptor;
25+
use TYPO3\CMS\Core\Console\CommandRegistry;
26+
27+
/**
28+
* ListCommand displays the list of all available commands for the application.
29+
*/
6D4E 30+
class ListCommand extends SymfonyListCommand
31+
{
32+
protected CommandRegistry $commandRegistry;
33+
34+
public function __construct(CommandRegistry $commandRegistry)
35+
{
36+
$this->commandRegistry = $commandRegistry;
37+
parent::__construct();
38+
}
39+
40+
/**
41+
* {@inheritdoc}
42+
*/
43+
protected function execute(InputInterface $input, OutputInterface $output)
44+
{
45+
$helper = new DescriptorHelper();
46+
$helper->register('txt', new TextDescriptor($this->commandRegistry));
47+
$helper->describe($output, $this->getApplication(), [
48+
'format' => $input->getOption('format'),
49+
'raw_text' => $input->getOption('raw'),
50+
'namespace' => $input->getArgument('namespace'),
51+
]);
52+
53+
return 0;
54+
}
55+
}

Classes/Command/SendEmailCommand.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ class SendEmailCommand extends Command
4242
protected function configure()
4343
{
4444
$this
45-
->setDescription('Sends emails from the spool')
4645
->addOption('message-limit', null, InputOption::VALUE_REQUIRED, 'The maximum number of messages to send.')
4746
->addOption('time-limit', null, InputOption::VALUE_REQUIRED, 'The time limit for sending messages (in seconds).')
4847
->addOption('recover-timeout', null, InputOption::VALUE_REQUIRED, 'The timeout for recovering messages that have taken too long to send (in seconds).')

Classes/Command/SiteListCommand.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,6 @@ public function __construct(SiteFinder $siteFinder)
4040
parent::__construct();
4141
}
4242

43-
/**
44-
* Defines the allowed options for this command
45-
*/
46-
protected function configure()
47-
{
48-
$this->setDescription('Shows the list of sites available to the system.');
49-
}
50-
5143
/**
5244
* Shows a table with all configured sites
5345
*

Classes/Command/SiteShowCommand.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,11 @@ public function __construct(SiteFinder $siteFinder)
4646
*/
4747
protected function configure()
4848
{
49-
$this->setDescription('Shows the configuration of the specified site. Specify the identifier via "site:show <identifier>".')
50-
->addArgument(
51-
'identifier',
52-
InputArgument::REQUIRED,
53-
'The identifier of the site'
54-
);
49+
$this->addArgument(
50+
'identifier',
51+
InputArgument::REQUIRED,
52+
'The identifier of the site'
53+
);
5554
}
5655

5756
/**

Classes/Console/CommandApplication.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public function __construct(Context $context, CommandRegistry $commandRegistry)
6565
));
6666
$this->application->setAutoExit(false);
6767
$this->application->setCommandLoader($commandRegistry);
68+
// Replace default list command with TYPO3 override
69+
$this->application->add($commandRegistry->get('list'));
6870
}
6971

7072
/**

Classes/Console/CommandRegistry.php

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Psr\Container\ContainerInterface;
2121
use Symfony\Component\Console\Command\Command;
2222
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
23+
use Symfony\Component\Console\Descriptor\ApplicationDescription;
2324
use Symfony\Component\Console\Exception\CommandNotFoundException;
2425
use TYPO3\CMS\Core\SingletonInterface;
2526

@@ -40,6 +41,13 @@ class CommandRegistry implements CommandLoaderInterface, SingletonInterface
4041
*/
4142
protected $commandConfigurations = [];
4243

44+
/**
45+
* Map of command aliases
46+
*
47+
* @var array[]
48+
*/
49+
protected $aliases = [];
50+
4351
/**
4452
* @param ContainerInterface $container
4553
*/
@@ -115,11 +123,98 @@ protected function getInstance(string $service): Command
115123
/**
116124
* @internal
117125
*/
118-
public function addLazyCommand(string $commandName, string $serviceName, bool $schedulable = true): void
126+
public function getNamespaces(): array
127+
{
128+
$namespaces = [];
129+
foreach ($this->commandConfigurations as $commandName => $configuration) {
130+
if ($configuration['hidden']) {
131+
continue;
132+
}
133+
if ($configuration['aliasFor'] !== null) {
134+
continue;
135+
}
136+
$namespace = $configuration['namespace'];
137+
$namespaces[$namespace]['id'] = $namespace;
138+
$namespaces[$namespace]['commands'][] = $commandName;
139+
}
140+
141+
ksort($namespaces);
142+
foreach ($namespaces as &$commands) {
143+
ksort($commands);
144+
}
145+
146+
return $namespaces;
147+
}
148+
149+
/**
150+
* Gets the commands (registered in the given namespace if provided).
151+
*
152+
* The array keys are the full names and the values the command instances.
153+
*
154+
* @return array An array of Command descriptors
155+
* @internal
156+
*/
157+
public function filter(string $namespace = null): array
119158
{
159+
$commands = [];
160+
foreach ($this->commandConfigurations as $commandName => $configuration) {
161+
if ($configuration['hidden']) {
162+
continue;
163+
}
164+
if ($namespace !== null && $namespace !== $this->extractNamespace($commandName, substr_count($namespace, ':') + 1)) {
165+
continue;
166+
}
167+
if ($configuration['aliasFor'] !== null) {
168+
continue;
169+
}
170+
171+
$commands[$commandName] = $configuration;
172+
$commands[$commandName]['aliases'] = $this->aliases[$commandName] ?? [];
173+
}
174+
175+
return $commands;
176+
}
177+
178+
/**
179+
* @internal
180+
*/
181+
public function addLazyCommand(
182+
string $commandName,
183+
string $serviceName,
184+
string $description = null,
185+
bool $hidden = false,
186+
bool $schedulable = false,
187+
string $aliasFor = null
188+
): void {
120189
$this->commandConfigurations[$commandName] = [
190+
'name' => $aliasFor ?? $commandName,
121191
'serviceName' => $serviceName,
192+
'description' => $description,
193+
'hidden' => $hidden,
122194
'schedulable' => $schedulable,
195+
'aliasFor' => $aliasFor,
196+
'namespace' => $this->extractNamespace($commandName, 1),
123197
];
198+
199+
if ($aliasFor !== null) {
200+
$this->aliases[$aliasFor][] = $commandName;
201+
}
202+
}
203+
204+
/**
205+
* Returns the namespace part of the command name.
206+
*
207+
* This method is not part of public API and should not be used directly.
208+
*
209+
* @return string The namespace of the command
210+
*/
211+
private function extractNamespace(string $name, int $limit = null): string
212+
{
213+
$parts = explode(':', $name, -1);
214+
if (count($parts) === 0) {
215+
return ApplicationDescription::GLOBAL_NAMESPACE;
216+
}
217+
218+
return implode(':', $limit === null ? $parts : array_slice($parts, 0, $limit));
124219
}
125220
}

0 commit comments

Comments
 (0)
0