Navigation.php
6.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
<?php
declare(strict_types=1);
namespace DrupalCodeGenerator\Command;
use DrupalCodeGenerator\InputOutput\DefaultOptions;
use DrupalCodeGenerator\InputOutput\IOAwareInterface;
use DrupalCodeGenerator\InputOutput\IOAwareTrait;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
/**
* Implements navigation command.
*/
#[AsCommand(name: 'navigation')]
final class Navigation extends Command implements IOAwareInterface, LoggerAwareInterface {
use IOAwareTrait;
use LoggerAwareTrait;
/**
* Menu tree.
*/
private array $menuTree = [];
/**
* Menu labels.
*/
private array $labels = [
'misc:d7' => 'Drupal 7',
'yml' => 'Yaml',
'misc' => 'Miscellaneous',
];
/**
* {@inheritdoc}
*/
protected function configure(): void {
// As the navigation is default command the help should be relevant to the
// entire DCG application.
$help = <<<'EOT'
<info>dcg</info> Display navigation
<info>dcg plugin:field:widget</info> Run a specific generator
<info>dcg list</info> List all available generators
EOT;
$this
->setName('navigation')
->setDescription('Command line code generator')
->setHelp($help)
->setHidden();
DefaultOptions::apply($this);
}
/**
* {@inheritdoc}
*/
public function getSynopsis($short = FALSE): string {
return 'dcg [options] <generator>';
}
/**
* {@inheritdoc}
*/
protected function initialize(InputInterface $input, OutputInterface $output): void {
parent::initialize($input, $output);
// Build the menu structure.
$this->menuTree = [];
if (!$application = $this->getApplication()) {
throw new \LogicException('Navigation command cannot work without application');
}
foreach ($application->all() as $command) {
if ($command instanceof LabelInterface && !$command->isHidden()) {
/** @var string $command_name */
$command_name = $command->getName();
self::arraySetNestedValue($this->menuTree, \explode(':', $command_name));
// Collect command labels.
if ($label = $command->getLabel()) {
$this->labels[$command_name] = $label;
}
}
}
self::recursiveKsort($this->menuTree);
$style = new OutputFormatterStyle('white', 'blue', ['bold']);
$output->getFormatter()->setStyle('title', $style);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
if ($command_name = $this->selectGenerator($input, $output)) {
if (!$application = $this->getApplication()) {
throw new \LogicException('Navigation command cannot work without application');
}
return $application
->find($command_name)
->run($input, $output);
}
return 0;
}
/**
* Selects a generator.
*
* Returns a generator selected by the user from a multilevel console menu or
* null if user decided to exit the navigation.
*/
private function selectGenerator(InputInterface $input, OutputInterface $output, array $menu_trail = []): ?string {
// Narrow down the menu tree using menu trail.
$active_menu_tree = $this->menuTree;
foreach ($menu_trail as $active_menu_item) {
$active_menu_tree = $active_menu_tree[$active_menu_item];
}
// The $active_menu_tree can be either an array of menu items or TRUE if the
// user has reached the final menu point.
if ($active_menu_tree === TRUE) {
return \implode(':', $menu_trail);
}
$sub_menu_labels = $command_labels = [];
foreach ($active_menu_tree as $menu_item => $subtree) {
$command_name = $menu_trail ? (\implode(':', $menu_trail) . ':' . $menu_item) : $menu_item;
$label = $this->labels[$command_name] ?? \str_replace(['-', '_'], ' ', \ucfirst($menu_item));
\is_array($subtree)
? $sub_menu_labels[$menu_item] = "<comment>$label</comment>"
: $command_labels[$menu_item] = $label;
}
// Generally the choices array consists of the following parts:
// - Reference to the parent menu level.
// - Sorted list of nested menu levels.
// - Sorted list of commands.
\natcasesort($sub_menu_labels);
\natcasesort($command_labels);
$choices = ['..' => '..'] + $sub_menu_labels + $command_labels;
$question = new ChoiceQuestion('<title> Select generator </title>', \array_values($choices));
$answer_label = $this->getHelper('question')->ask($input, $output, $question);
$answer = \array_search($answer_label, $choices);
if ($answer === '..') {
// Exit the application if a user selected zero on the top menu level.
if (\count($menu_trail) === 0) {
return NULL;
}
// Level up.
\array_pop($menu_trail);
}
else {
// Level down.
$menu_trail[] = $answer;
}
return $this->selectGenerator($input, $output, $menu_trail);
}
/**
* Sort multidimensional array by keys.
*
* @param array $array
* An array being sorted.
*/
private static function recursiveKsort(array &$array): void {
foreach ($array as &$value) {
if (\is_array($value)) {
self::recursiveKsort($value);
}
}
\ksort($array);
}
/**
* Sets the property to true in nested array.
*
* @psalm-param list<string> $parents
* An array of parent keys, starting with the outermost key.
*
* @see https://api.drupal.org/api/drupal/includes!common.inc/function/drupal_array_set_nested_value/7
*/
private static function arraySetNestedValue(array &$array, array $parents): void {
$ref = &$array;
foreach ($parents as $parent) {
if (isset($ref) && !\is_array($ref)) {
$ref = [];
}
// @todo Fix this.
/** @psalm-suppress PossiblyNullArrayAccess */
$ref = &$ref[$parent];
}
$ref ??= TRUE;
}
}