Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 namespace DrupalCodeGenerator\Command;
|
Chris@0
|
4
|
Chris@0
|
5 use Symfony\Component\Console\Command\Command;
|
Chris@0
|
6 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
|
Chris@0
|
7 use Symfony\Component\Console\Input\InputInterface;
|
Chris@0
|
8 use Symfony\Component\Console\Input\InputOption;
|
Chris@0
|
9 use Symfony\Component\Console\Output\OutputInterface;
|
Chris@0
|
10 use Symfony\Component\Console\Question\ChoiceQuestion;
|
Chris@0
|
11
|
Chris@0
|
12 /**
|
Chris@0
|
13 * Implements generate command.
|
Chris@0
|
14 */
|
Chris@0
|
15 class Navigation extends Command {
|
Chris@0
|
16
|
Chris@0
|
17 /**
|
Chris@0
|
18 * Menu tree.
|
Chris@0
|
19 *
|
Chris@0
|
20 * @var array
|
Chris@0
|
21 */
|
Chris@0
|
22 protected $menuTree;
|
Chris@0
|
23
|
Chris@0
|
24 /**
|
Chris@0
|
25 * Name of the generator to execute.
|
Chris@0
|
26 *
|
Chris@0
|
27 * @var string
|
Chris@0
|
28 */
|
Chris@0
|
29 protected $generatorName;
|
Chris@0
|
30
|
Chris@0
|
31 /**
|
Chris@0
|
32 * Command labels.
|
Chris@0
|
33 *
|
Chris@0
|
34 * @var array
|
Chris@0
|
35 */
|
Chris@0
|
36 protected $labels = [
|
Chris@0
|
37 'd7' => 'Drupal 7',
|
Chris@0
|
38 'd8' => 'Drupal 8',
|
Chris@0
|
39 ];
|
Chris@0
|
40
|
Chris@0
|
41 /**
|
Chris@0
|
42 * Aliases for some sub-menus.
|
Chris@0
|
43 *
|
Chris@0
|
44 * @var array
|
Chris@0
|
45 */
|
Chris@0
|
46 protected $defaultAliases = [
|
Chris@0
|
47 'service' => 'd8:service',
|
Chris@0
|
48 'plugin' => 'd8:plugin',
|
Chris@0
|
49 'theme' => 'd8:theme',
|
Chris@0
|
50 'module' => 'd8:module',
|
Chris@0
|
51 'form' => 'd8:form',
|
Chris@0
|
52 'test' => 'd8:test',
|
Chris@0
|
53 'yml' => 'd8:yml',
|
Chris@0
|
54 'links' => 'd8:yml:links',
|
Chris@0
|
55 ];
|
Chris@0
|
56
|
Chris@0
|
57 /**
|
Chris@0
|
58 * Constructs menu command.
|
Chris@0
|
59 *
|
Chris@0
|
60 * @param \DrupalCodeGenerator\Command\GeneratorInterface[] $commands
|
Chris@0
|
61 * List of registered commands.
|
Chris@0
|
62 */
|
Chris@0
|
63 public function __construct(array $commands) {
|
Chris@0
|
64 parent::__construct();
|
Chris@0
|
65
|
Chris@0
|
66 // Initialize the menu structure.
|
Chris@0
|
67 $this->menuTree = [];
|
Chris@0
|
68 $aliases = array_keys($this->defaultAliases);
|
Chris@0
|
69
|
Chris@0
|
70 // Build aliases for the navigation based on command namespaces.
|
Chris@0
|
71 foreach ($commands as $command) {
|
Chris@0
|
72 $command_name = $command->getName();
|
Chris@0
|
73 $sub_names = explode(':', $command_name);
|
Chris@0
|
74
|
Chris@0
|
75 $this->arraySetNestedValue($this->menuTree, $sub_names, TRUE);
|
Chris@0
|
76
|
Chris@0
|
77 // The last sub-name is actual command name so it cannot be used as an
|
Chris@0
|
78 // alias for navigation command.
|
Chris@0
|
79 $last_sub_name = array_pop($sub_names);
|
Chris@0
|
80
|
Chris@0
|
81 // Collect command labels.
|
Chris@0
|
82 if ($label = $command->getLabel()) {
|
Chris@0
|
83 $this->labels[$last_sub_name] = $label;
|
Chris@0
|
84 }
|
Chris@0
|
85
|
Chris@0
|
86 // We cannot use $application->getNamespaces() here because the
|
Chris@0
|
87 // application is not available at this point.
|
Chris@0
|
88 $alias = '';
|
Chris@0
|
89 foreach ($sub_names as $sub_name) {
|
Chris@0
|
90 $alias = $alias ? $alias . ':' . $sub_name : $sub_name;
|
Chris@0
|
91 $aliases[] = $alias;
|
Chris@0
|
92 }
|
Chris@0
|
93 }
|
Chris@0
|
94
|
Chris@0
|
95 $this->setAliases(array_unique($aliases));
|
Chris@0
|
96 $this->recursiveKsort($this->menuTree);
|
Chris@0
|
97 }
|
Chris@0
|
98
|
Chris@0
|
99 /**
|
Chris@0
|
100 * {@inheritdoc}
|
Chris@0
|
101 */
|
Chris@0
|
102 public function getUsages() {
|
Chris@0
|
103 return ['<generator>'];
|
Chris@0
|
104 }
|
Chris@0
|
105
|
Chris@0
|
106 /**
|
Chris@0
|
107 * {@inheritdoc}
|
Chris@0
|
108 */
|
Chris@0
|
109 protected function configure() {
|
Chris@0
|
110 $this
|
Chris@0
|
111 ->setName('navigation')
|
Chris@0
|
112 ->setDescription('Provides an interactive menu to select generator')
|
Chris@0
|
113 ->setHelp('Run `dcg list` to check out all available generators.')
|
Chris@0
|
114 ->setHidden(TRUE)
|
Chris@0
|
115 ->addOption(
|
Chris@0
|
116 'directory',
|
Chris@0
|
117 '-d',
|
Chris@0
|
118 InputOption::VALUE_OPTIONAL,
|
Chris@0
|
119 'Working directory'
|
Chris@0
|
120 )
|
Chris@0
|
121 ->addOption(
|
Chris@0
|
122 'answers',
|
Chris@0
|
123 '-a',
|
Chris@0
|
124 InputOption::VALUE_OPTIONAL,
|
Chris@0
|
125 'Default JSON formatted answers'
|
Chris@0
|
126 );
|
Chris@0
|
127 }
|
Chris@0
|
128
|
Chris@0
|
129 /**
|
Chris@0
|
130 * {@inheritdoc}
|
Chris@0
|
131 */
|
Chris@0
|
132 protected function interact(InputInterface $input, OutputInterface $output) {
|
Chris@0
|
133 $style = new OutputFormatterStyle('black', 'cyan', []);
|
Chris@0
|
134 $output->getFormatter()->setStyle('title', $style);
|
Chris@0
|
135
|
Chris@0
|
136 $command_name = $input->getFirstArgument();
|
Chris@0
|
137
|
Chris@0
|
138 // Before version 3.3.6 of Symfony console getFistArgument returned default
|
Chris@0
|
139 // command name.
|
Chris@0
|
140 $command_name = $command_name == 'navigation' ? NULL : $command_name;
|
Chris@0
|
141
|
Chris@0
|
142 if (isset($this->defaultAliases[$command_name])) {
|
Chris@0
|
143 $command_name = $this->defaultAliases[$command_name];
|
Chris@0
|
144 }
|
Chris@0
|
145
|
Chris@0
|
146 $menu_trail = $command_name ? explode(':', $command_name) : [];
|
Chris@0
|
147
|
Chris@0
|
148 $this->generatorName = $this->selectGenerator($input, $output, $menu_trail);
|
Chris@0
|
149 }
|
Chris@0
|
150
|
Chris@0
|
151 /**
|
Chris@0
|
152 * {@inheritdoc}
|
Chris@0
|
153 */
|
Chris@0
|
154 protected function execute(InputInterface $input, OutputInterface $output) {
|
Chris@0
|
155
|
Chris@0
|
156 if (!$this->generatorName) {
|
Chris@0
|
157 return 0;
|
Chris@0
|
158 }
|
Chris@0
|
159
|
Chris@0
|
160 // Run the generator.
|
Chris@0
|
161 return $this->getApplication()
|
Chris@0
|
162 ->find($this->generatorName)
|
Chris@0
|
163 ->run($input, $output);
|
Chris@0
|
164 }
|
Chris@0
|
165
|
Chris@0
|
166 /**
|
Chris@0
|
167 * Returns a generator selected by the user from a multilevel console menu.
|
Chris@0
|
168 *
|
Chris@0
|
169 * @param \Symfony\Component\Console\Input\InputInterface $input
|
Chris@0
|
170 * Input instance.
|
Chris@0
|
171 * @param \Symfony\Component\Console\Output\OutputInterface $output
|
Chris@0
|
172 * Output instance.
|
Chris@0
|
173 * @param array $menu_trail
|
Chris@0
|
174 * Menu trail.
|
Chris@0
|
175 *
|
Chris@0
|
176 * @return string|null
|
Chris@0
|
177 * Generator name or null if user decided to exit the navigation.
|
Chris@0
|
178 */
|
Chris@0
|
179 protected function selectGenerator(InputInterface $input, OutputInterface $output, array $menu_trail) {
|
Chris@0
|
180
|
Chris@0
|
181 // Narrow down menu tree.
|
Chris@0
|
182 $active_menu_tree = $this->menuTree;
|
Chris@0
|
183 foreach ($menu_trail as $active_menu_item) {
|
Chris@0
|
184 $active_menu_tree = $active_menu_tree[$active_menu_item];
|
Chris@0
|
185 }
|
Chris@0
|
186
|
Chris@0
|
187 // The $active_menu_tree can be either an array of menu items or TRUE if the
|
Chris@0
|
188 // user reached the final menu point.
|
Chris@0
|
189 if ($active_menu_tree === TRUE) {
|
Chris@0
|
190 return implode(':', $menu_trail);
|
Chris@0
|
191 }
|
Chris@0
|
192
|
Chris@0
|
193 $sub_menu_labels = $command_labels = [];
|
Chris@0
|
194 foreach ($active_menu_tree as $menu_item => $subtree) {
|
Chris@0
|
195 if (is_array($subtree)) {
|
Chris@0
|
196 $sub_menu_labels[$menu_item] = $this->createMenuItemLabel($menu_item, TRUE);
|
Chris@0
|
197 }
|
Chris@0
|
198 else {
|
Chris@0
|
199 $command_labels[$menu_item] = $this->createMenuItemLabel($menu_item, FALSE);
|
Chris@0
|
200 }
|
Chris@0
|
201 }
|
Chris@0
|
202 asort($sub_menu_labels);
|
Chris@0
|
203 asort($command_labels);
|
Chris@0
|
204
|
Chris@0
|
205 // Generally the choices array consists of the following parts:
|
Chris@0
|
206 // - Reference to the parent menu level.
|
Chris@0
|
207 // - Sorted list of nested menu levels.
|
Chris@0
|
208 // - Sorted list of commands.
|
Chris@0
|
209 $choices = ['..' => '..'] + $sub_menu_labels + $command_labels;
|
Chris@0
|
210 $question = new ChoiceQuestion('<title> Select generator: </title>', array_values($choices));
|
Chris@0
|
211 $question->setPrompt(count($choices) <= 10 ? ' ➤➤➤ ' : ' ➤➤➤➤ ');
|
Chris@0
|
212
|
Chris@0
|
213 $answer_label = $this->getHelper('question')->ask($input, $output, $question);
|
Chris@0
|
214 $answer = array_search($answer_label, $choices);
|
Chris@0
|
215
|
Chris@0
|
216 if ($answer == '..') {
|
Chris@0
|
217 // Exit the application if the user selected zero on the top menu level.
|
Chris@0
|
218 if (count($menu_trail) == 0) {
|
Chris@0
|
219 return NULL;
|
Chris@0
|
220 }
|
Chris@0
|
221 // Decrease menu level.
|
Chris@0
|
222 array_pop($menu_trail);
|
Chris@0
|
223 }
|
Chris@0
|
224 else {
|
Chris@0
|
225 // Increase menu level.
|
Chris@0
|
226 $menu_trail[] = $answer;
|
Chris@0
|
227 }
|
Chris@0
|
228
|
Chris@0
|
229 return $this->selectGenerator($input, $output, $menu_trail);
|
Chris@0
|
230 }
|
Chris@0
|
231
|
Chris@0
|
232 /**
|
Chris@0
|
233 * Creates a human readable label for a given menu item.
|
Chris@0
|
234 *
|
Chris@0
|
235 * @param string $menu_item
|
Chris@0
|
236 * Machine name of the menu item.
|
Chris@0
|
237 * @param bool $comment
|
Chris@0
|
238 * A boolean indicating that the label should be wrapped with comment tag.
|
Chris@0
|
239 *
|
Chris@0
|
240 * @return string
|
Chris@0
|
241 * The menu label.
|
Chris@0
|
242 */
|
Chris@0
|
243 protected function createMenuItemLabel($menu_item, $comment) {
|
Chris@0
|
244 $label = isset($this->labels[$menu_item]) ?
|
Chris@0
|
245 $this->labels[$menu_item] : str_replace(['-', '_'], ' ', ucfirst($menu_item));
|
Chris@0
|
246 return $comment ? "<comment>$label</comment>" : $label;
|
Chris@0
|
247 }
|
Chris@0
|
248
|
Chris@0
|
249 /**
|
Chris@0
|
250 * Sort multi-dimensional array by keys.
|
Chris@0
|
251 *
|
Chris@0
|
252 * @param array $array
|
Chris@0
|
253 * An array being sorted.
|
Chris@0
|
254 *
|
Chris@0
|
255 * @return array
|
Chris@0
|
256 * Sorted array.
|
Chris@0
|
257 */
|
Chris@0
|
258 protected function recursiveKsort(array &$array) {
|
Chris@0
|
259 foreach ($array as &$value) {
|
Chris@0
|
260 if (is_array($value)) {
|
Chris@0
|
261 $this->recursiveKsort($value);
|
Chris@0
|
262 }
|
Chris@0
|
263 }
|
Chris@0
|
264 return ksort($array);
|
Chris@0
|
265 }
|
Chris@0
|
266
|
Chris@0
|
267 /**
|
Chris@0
|
268 * Sets a value in a nested array with variable depth.
|
Chris@0
|
269 *
|
Chris@0
|
270 * @param array $array
|
Chris@0
|
271 * A reference to the array to modify.
|
Chris@0
|
272 * @param array $parents
|
Chris@0
|
273 * An array of parent keys, starting with the outermost key.
|
Chris@0
|
274 * @param mixed $value
|
Chris@0
|
275 * The value to set.
|
Chris@0
|
276 *
|
Chris@0
|
277 * @see https://api.drupal.org/api/drupal/includes!common.inc/function/drupal_array_set_nested_value/7
|
Chris@0
|
278 */
|
Chris@0
|
279 protected function arraySetNestedValue(array &$array, array $parents, $value) {
|
Chris@0
|
280 $ref = &$array;
|
Chris@0
|
281 foreach ($parents as $parent) {
|
Chris@0
|
282 if (isset($ref) && !is_array($ref)) {
|
Chris@0
|
283 $ref = [];
|
Chris@0
|
284 }
|
Chris@0
|
285 $ref = &$ref[$parent];
|
Chris@0
|
286 }
|
Chris@0
|
287 if (!isset($ref)) {
|
Chris@0
|
288 $ref = $value;
|
Chris@0
|
289 }
|
Chris@0
|
290 }
|
Chris@0
|
291
|
Chris@0
|
292 }
|