1
|
|
|
<?php |
2
|
|
|
namespace Consolidation\AnnotatedCommand; |
3
|
|
|
|
4
|
|
|
use Consolidation\AnnotatedCommand\Hooks\HookManager; |
5
|
|
|
use Consolidation\AnnotatedCommand\Parser\CommandInfo; |
6
|
|
|
use Consolidation\OutputFormatters\FormatterManager; |
7
|
|
|
use Consolidation\OutputFormatters\Options\FormatterOptions; |
8
|
|
|
use Consolidation\AnnotatedCommand\Help\HelpDocumentAlter; |
9
|
|
|
use Psr\Log\LoggerInterface; |
10
|
|
|
use Symfony\Component\Console\Command\Command; |
11
|
|
|
use Symfony\Component\Console\Input\InputArgument; |
12
|
|
|
use Symfony\Component\Console\Input\InputInterface; |
13
|
|
|
use Symfony\Component\Console\Input\InputOption; |
14
|
|
|
use Symfony\Component\Console\Output\OutputInterface; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* AnnotatedCommands are created automatically by the |
18
|
|
|
* AnnotatedCommandFactory. Each command method in a |
19
|
|
|
* command file will produce one AnnotatedCommand. These |
20
|
|
|
* are then added to your Symfony Console Application object; |
21
|
|
|
* nothing else is needed. |
22
|
|
|
* |
23
|
|
|
* Optionally, though, you may extend AnnotatedCommand directly |
24
|
|
|
* to make a single command. The usage pattern is the same |
25
|
|
|
* as for any other Symfony Console command, except that you may |
26
|
|
|
* omit the 'Confiure' method, and instead place your annotations |
27
|
|
|
* on the execute() method. |
28
|
|
|
* |
29
|
|
|
* @package Consolidation\AnnotatedCommand |
30
|
|
|
*/ |
31
|
|
|
class AnnotatedCommand extends Command implements HelpDocumentAlter |
32
|
|
|
{ |
33
|
|
|
protected $calls = []; |
34
|
|
|
protected $commandCallback; |
35
|
|
|
protected $commandProcessor; |
36
|
|
|
protected $annotationData; |
37
|
|
|
protected $examples = []; |
38
|
|
|
protected $topics = []; |
39
|
|
|
protected $usesInputInterface; |
40
|
|
|
protected $usesOutputInterface; |
41
|
|
|
protected $returnType; |
42
|
|
|
|
43
|
|
|
public function __construct($name = null) |
44
|
|
|
{ |
45
|
|
|
$commandInfo = false; |
46
|
|
|
|
47
|
|
|
// If this is a subclass of AnnotatedCommand, check to see |
48
|
|
|
// if the 'execute' method is annotated. We could do this |
49
|
|
|
// unconditionally; it is a performance optimization to skip |
50
|
|
|
// checking the annotations if $this is an instance of |
51
|
|
|
// AnnotatedCommand. Alternately, we break out a new subclass. |
52
|
|
|
// The command factory instantiates the subclass. |
53
|
|
|
if (get_class($this) != 'Consolidation\AnnotatedCommand\AnnotatedCommand') { |
54
|
|
|
$commandInfo = new CommandInfo($this, 'execute'); |
55
|
|
|
if (!isset($name)) { |
56
|
|
|
$name = $commandInfo->getName(); |
57
|
|
|
} |
58
|
|
|
} |
59
|
|
|
parent::__construct($name); |
60
|
|
|
if ($commandInfo && $commandInfo->hasAnnotation('command')) { |
61
|
|
|
$this->setCommandInfo($commandInfo); |
62
|
|
|
$this->setCommandOptions($commandInfo); |
63
|
|
|
} |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
public function setCommandCallback($commandCallback) |
67
|
|
|
{ |
68
|
|
|
$this->commandCallback = $commandCallback; |
69
|
|
|
return $this; |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
public function setCommandProcessor($commandProcessor) |
73
|
|
|
{ |
74
|
|
|
$this->commandProcessor = $commandProcessor; |
75
|
|
|
return $this; |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
public function commandProcessor() |
79
|
|
|
{ |
80
|
|
|
// If someone is using an AnnotatedCommand, and is NOT getting |
81
|
|
|
// it from an AnnotatedCommandFactory OR not correctly injecting |
82
|
|
|
// a command processor via setCommandProcessor() (ideally via the |
83
|
|
|
// DI container), then we'll just give each annotated command its |
84
|
|
|
// own command processor. This is not ideal; preferably, there would |
85
|
|
|
// only be one instance of the command processor in the application. |
86
|
|
|
if (!isset($this->commandProcessor)) { |
87
|
|
|
$this->commandProcessor = new CommandProcessor(new HookManager()); |
88
|
|
|
} |
89
|
|
|
return $this->commandProcessor; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
public function getReturnType() |
93
|
|
|
{ |
94
|
|
|
return $this->returnType; |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
public function setReturnType($returnType) |
98
|
|
|
{ |
99
|
|
|
$this->returnType = $returnType; |
100
|
|
|
return $this; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
public function getAnnotationData() |
104
|
|
|
{ |
105
|
|
|
return $this->annotationData; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
public function setAnnotationData($annotationData) |
109
|
|
|
{ |
110
|
|
|
$this->annotationData = $annotationData; |
111
|
|
|
return $this; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
public function getTopics() |
115
|
|
|
{ |
116
|
|
|
return $this->topics; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
public function setTopics($topics) |
120
|
|
|
{ |
121
|
|
|
$this->topics = $topics; |
122
|
|
|
return $this; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
public function getCalls() |
126
|
|
|
{ |
127
|
|
|
return $this->calls; |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
public function setCalls($calls) |
131
|
|
|
{ |
132
|
|
|
$this->calls = $calls; |
133
|
|
|
return $this; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
public function setCommandInfo($commandInfo) |
137
|
|
|
{ |
138
|
|
|
$this->setCalls($commandInfo->getCalls()); |
139
|
|
|
$this->setDescription($commandInfo->getDescription()); |
140
|
|
|
$this->setHelp($commandInfo->getHelp()); |
141
|
|
|
$this->setAliases($commandInfo->getAliases()); |
142
|
|
|
$this->setAnnotationData($commandInfo->getAnnotations()); |
143
|
|
|
$this->setTopics($commandInfo->getTopics()); |
144
|
|
|
foreach ($commandInfo->getExampleUsages() as $usage => $description) { |
145
|
|
|
$this->addUsageOrExample($usage, $description); |
146
|
|
|
} |
147
|
|
|
$this->setCommandArguments($commandInfo); |
148
|
|
|
$this->setReturnType($commandInfo->getReturnType()); |
149
|
|
|
return $this; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
protected function addUsageOrExample($usage, $description) |
153
|
|
|
{ |
154
|
|
|
$this->addUsage($usage); |
155
|
|
|
if (!empty($description)) { |
156
|
|
|
$this->examples[$usage] = $description; |
157
|
|
|
} |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
public function helpAlter(\DomDocument $originalDom) |
161
|
|
|
{ |
162
|
|
|
$dom = new \DOMDocument('1.0', 'UTF-8'); |
163
|
|
|
$dom->appendChild($commandXML = $dom->createElement('command')); |
164
|
|
|
$commandXML->setAttribute('id', $this->getName()); |
165
|
|
|
$commandXML->setAttribute('name', $this->getName()); |
166
|
|
|
|
167
|
|
|
// Get the original <command> element and its top-level elements. |
168
|
|
|
$originalCommandXML = $this->getSingleElementByTagName($dom, $originalDom, 'command'); |
169
|
|
|
$originalUsagesXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'usages'); |
170
|
|
|
$originalDescriptionXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'description'); |
171
|
|
|
$originalHelpXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'help'); |
172
|
|
|
$originalArgumentsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'arguments'); |
173
|
|
|
$originalOptionsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'options'); |
174
|
|
|
|
175
|
|
|
// Keep only the first of the <usage> elements |
176
|
|
|
$newUsagesXML = $dom->createElement('usages'); |
177
|
|
|
$firstUsageXML = $this->getSingleElementByTagName($dom, $originalUsagesXML, 'usage'); |
178
|
|
|
$newUsagesXML->appendChild($firstUsageXML); |
179
|
|
|
|
180
|
|
|
// Create our own <example> elements |
181
|
|
|
$newExamplesXML = $dom->createElement('examples'); |
182
|
|
|
foreach ($this->examples as $usage => $description) { |
183
|
|
|
$newExamplesXML->appendChild($exampleXML = $dom->createElement('example')); |
184
|
|
|
$exampleXML->appendChild($usageXML = $dom->createElement('usage', $usage)); |
185
|
|
|
$exampleXML->appendChild($descriptionXML = $dom->createElement('description', $description)); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
// Create our own <alias> elements |
189
|
|
|
$newAliasesXML = $dom->createElement('aliases'); |
190
|
|
|
foreach ($this->getAliases() as $alias) { |
191
|
|
|
$newAliasesXML->appendChild($dom->createElement('alias', $alias)); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
// Create our own <topic> elements |
195
|
|
|
$newTopicsXML = $dom->createElement('topics'); |
196
|
|
|
foreach ($this->getTopics() as $topic) { |
197
|
|
|
$newTopicsXML->appendChild($topicXML = $dom->createElement('topic', $topic)); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
// Place the different elements into the <command> element in the desired order |
201
|
|
|
$commandXML->appendChild($newUsagesXML); |
202
|
|
|
$commandXML->appendChild($newExamplesXML); |
203
|
|
|
$commandXML->appendChild($originalDescriptionXML); |
204
|
|
|
$commandXML->appendChild($originalArgumentsXML); |
205
|
|
|
$commandXML->appendChild($originalOptionsXML); |
206
|
|
|
$commandXML->appendChild($originalHelpXML); |
207
|
|
|
$commandXML->appendChild($newAliasesXML); |
208
|
|
|
$commandXML->appendChild($newTopicsXML); |
209
|
|
|
|
210
|
|
|
return $dom; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
protected function getSingleElementByTagName($dom, $parent, $tagName) |
214
|
|
|
{ |
215
|
|
|
// There should always be exactly one '<command>' element. |
216
|
|
|
$elements = $parent->getElementsByTagName($tagName); |
217
|
|
|
$result = $elements->item(0); |
218
|
|
|
|
219
|
|
|
$result = $dom->importNode($result, true); |
220
|
|
|
|
221
|
|
|
return $result; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
protected function setCommandArguments($commandInfo) |
225
|
|
|
{ |
226
|
|
|
$this->setUsesInputInterface($commandInfo); |
227
|
|
|
$this->setUsesOutputInterface($commandInfo); |
228
|
|
|
$this->setCommandArgumentsFromParameters($commandInfo); |
229
|
|
|
return $this; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Check whether the first parameter is an InputInterface. |
234
|
|
|
*/ |
235
|
|
|
protected function checkUsesInputInterface($params) |
236
|
|
|
{ |
237
|
|
|
$firstParam = reset($params); |
238
|
|
|
return $firstParam instanceof InputInterface; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Determine whether this command wants to get its inputs |
243
|
|
|
* via an InputInterface or via its command parameters |
244
|
|
|
*/ |
245
|
|
|
protected function setUsesInputInterface($commandInfo) |
246
|
|
|
{ |
247
|
|
|
$params = $commandInfo->getParameters(); |
248
|
|
|
$this->usesInputInterface = $this->checkUsesInputInterface($params); |
249
|
|
|
return $this; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Determine whether this command wants to send its output directly |
254
|
|
|
* to the provided OutputInterface, or whether it will returned |
255
|
|
|
* structured output to be processed by the command processor. |
256
|
|
|
*/ |
257
|
|
|
protected function setUsesOutputInterface($commandInfo) |
258
|
|
|
{ |
259
|
|
|
$params = $commandInfo->getParameters(); |
260
|
|
|
$index = $this->checkUsesInputInterface($params) ? 1 : 0; |
261
|
|
|
$this->usesOutputInterface = |
262
|
|
|
(count($params) > $index) && |
263
|
|
|
($params[$index] instanceof OutputInterface); |
264
|
|
|
return $this; |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
protected function setCommandArgumentsFromParameters($commandInfo) |
268
|
|
|
{ |
269
|
|
|
$args = $commandInfo->arguments()->getValues(); |
270
|
|
|
foreach ($args as $name => $defaultValue) { |
271
|
|
|
$description = $commandInfo->arguments()->getDescription($name); |
272
|
|
|
$hasDefault = $commandInfo->arguments()->hasDefault($name); |
273
|
|
|
$parameterMode = $this->getCommandArgumentMode($hasDefault, $defaultValue); |
274
|
|
|
$this->addArgument($name, $parameterMode, $description, $defaultValue); |
275
|
|
|
} |
276
|
|
|
return $this; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
protected function getCommandArgumentMode($hasDefault, $defaultValue) |
280
|
|
|
{ |
281
|
|
|
if (!$hasDefault) { |
282
|
|
|
return InputArgument::REQUIRED; |
283
|
|
|
} |
284
|
|
|
if (is_array($defaultValue)) { |
285
|
|
|
return InputArgument::IS_ARRAY; |
286
|
|
|
} |
287
|
|
|
return InputArgument::OPTIONAL; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
public function setCommandOptions($commandInfo, $automaticOptions = []) |
291
|
|
|
{ |
292
|
|
|
$inputOptions = $commandInfo->inputOptions(); |
293
|
|
|
|
294
|
|
|
$this->addOptions($inputOptions + $automaticOptions, $automaticOptions); |
295
|
|
|
return $this; |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
public function addOptions($inputOptions, $automaticOptions = []) |
299
|
|
|
{ |
300
|
|
|
foreach ($inputOptions as $name => $inputOption) { |
301
|
|
|
$description = $inputOption->getDescription(); |
302
|
|
|
|
303
|
|
|
if (empty($description) && isset($automaticOptions[$name])) { |
304
|
|
|
$description = $automaticOptions[$name]->getDescription(); |
305
|
|
|
$inputOption = static::inputOptionSetDescription($inputOption, $description); |
306
|
|
|
} |
307
|
|
|
$this->getDefinition()->addOption($inputOption); |
308
|
|
|
} |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
protected static function inputOptionSetDescription($inputOption, $description) |
312
|
|
|
{ |
313
|
|
|
// Recover the 'mode' value, because Symfony is stubborn |
314
|
|
|
$mode = 0; |
315
|
|
|
if ($inputOption->isValueRequired()) { |
316
|
|
|
$mode |= InputOption::VALUE_REQUIRED; |
317
|
|
|
} |
318
|
|
|
if ($inputOption->isValueOptional()) { |
319
|
|
|
$mode |= InputOption::VALUE_OPTIONAL; |
320
|
|
|
} |
321
|
|
|
if ($inputOption->isArray()) { |
322
|
|
|
$mode |= InputOption::VALUE_IS_ARRAY; |
323
|
|
|
} |
324
|
|
|
if (!$mode) { |
325
|
|
|
$mode = InputOption::VALUE_NONE; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
$inputOption = new InputOption( |
329
|
|
|
$inputOption->getName(), |
330
|
|
|
$inputOption->getShortcut(), |
331
|
|
|
$mode, |
332
|
|
|
$description, |
333
|
|
|
$inputOption->getDefault() |
334
|
|
|
); |
335
|
|
|
return $inputOption; |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
/** |
339
|
|
|
* Returns all of the hook names that may be called for this command. |
340
|
|
|
* |
341
|
|
|
* @return array |
342
|
|
|
*/ |
343
|
|
|
public function getNames() |
344
|
|
|
{ |
345
|
|
|
return HookManager::getNames($this, $this->commandCallback); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* Add any options to this command that are defined by hook implementations |
350
|
|
|
*/ |
351
|
|
|
public function optionsHook() |
352
|
|
|
{ |
353
|
|
|
$this->commandProcessor()->optionsHook( |
354
|
|
|
$this, |
355
|
|
|
$this->getNames(), |
356
|
|
|
$this->annotationData |
357
|
|
|
); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
public function optionsHookForHookAnnotations($commandInfoList) |
361
|
|
|
{ |
362
|
|
|
foreach ($commandInfoList as $commandInfo) { |
363
|
|
|
$inputOptions = $commandInfo->inputOptions(); |
364
|
|
|
$this->addOptions($inputOptions); |
365
|
|
|
foreach ($commandInfo->getExampleUsages() as $usage => $description) { |
366
|
|
|
if (!in_array($usage, $this->getUsages())) { |
367
|
|
|
$this->addUsageOrExample($usage, $description); |
368
|
|
|
} |
369
|
|
|
} |
370
|
|
|
} |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* {@inheritdoc} |
375
|
|
|
*/ |
376
|
|
|
protected function interact(InputInterface $input, OutputInterface $output) |
377
|
|
|
{ |
378
|
|
|
$this->commandProcessor()->interact( |
379
|
|
|
$input, |
380
|
|
|
$output, |
381
|
|
|
$this->getNames(), |
382
|
|
|
$this->annotationData |
383
|
|
|
); |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
protected function initialize(InputInterface $input, OutputInterface $output) |
387
|
|
|
{ |
388
|
|
|
// Allow the hook manager a chance to provide configuration values, |
389
|
|
|
// if there are any registered hooks to do that. |
390
|
|
|
$this->commandProcessor()->initializeHook($input, $this->getNames(), $this->annotationData); |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
protected function executeCallsCommands(InputInterface $input, OutputInterface $output) { |
394
|
|
|
if ($this->getCalls()) { |
395
|
|
|
//$logger = $this->getApplication()->get('logger'); |
|
|
|
|
396
|
|
|
foreach ($this->getCalls() as $command_name) { |
397
|
|
|
$command = $this->getApplication()->find($command_name); |
398
|
|
|
if (!$command) { |
399
|
|
|
/** @var LoggerInterface $logger */ |
400
|
|
|
//$logger->warning("Command $command_name does not exist. Skipping."); |
|
|
|
|
401
|
|
|
continue; |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
// @see http://symfony.com/doc/current/console/calling_commands.html |
405
|
|
|
try { |
406
|
|
|
$exit_code = $command->run($input, $output); |
407
|
|
|
// If command was not successful, return early. |
408
|
|
|
if ($exit_code !== 0) { |
409
|
|
|
return $exit_code; |
410
|
|
|
} |
411
|
|
|
} |
412
|
|
|
catch (\Exception $e) { |
413
|
|
|
//$logger->error("Exception was caught while executing $command_name."); |
|
|
|
|
414
|
|
|
// Return non-zero exit code. |
415
|
|
|
return 1; |
416
|
|
|
} |
417
|
|
|
} |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
// Return 0 exit code for success. |
421
|
|
|
return 0; |
422
|
|
|
} |
423
|
|
|
|
424
|
|
|
protected function executeCommand() { |
425
|
|
|
|
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
/** |
429
|
|
|
* {@inheritdoc} |
430
|
|
|
*/ |
431
|
|
|
protected function execute(InputInterface $input, OutputInterface $output) |
432
|
|
|
{ |
433
|
|
|
$calls_exit_code = $this->executeCallsCommands($input, $output); |
434
|
|
|
// If one of the "@calls" commands returned a non-zero exit code, |
435
|
|
|
// return early. |
436
|
|
|
if ($calls_exit_code !== 0) { |
437
|
|
|
return $calls_exit_code;; |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
// Validate, run, process, alter, handle results. |
441
|
|
|
return $this->commandProcessor()->process( |
442
|
|
|
$output, |
443
|
|
|
$this->getNames(), |
444
|
|
|
$this->commandCallback, |
445
|
|
|
$this->createCommandData($input, $output) |
446
|
|
|
); |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
/** |
450
|
|
|
* This function is available for use by a class that may |
451
|
|
|
* wish to extend this class rather than use annotations to |
452
|
|
|
* define commands. Using this technique does allow for the |
453
|
|
|
* use of annotations to define hooks. |
454
|
|
|
*/ |
455
|
|
|
public function processResults(InputInterface $input, OutputInterface $output, $results) |
456
|
|
|
{ |
457
|
|
|
$commandData = $this->createCommandData($input, $output); |
458
|
|
|
$commandProcessor = $this->commandProcessor(); |
459
|
|
|
$names = $this->getNames(); |
460
|
|
|
$results = $commandProcessor->processResults( |
461
|
|
|
$names, |
462
|
|
|
$results, |
463
|
|
|
$commandData |
464
|
|
|
); |
465
|
|
|
return $commandProcessor->handleResults( |
466
|
|
|
$output, |
467
|
|
|
$names, |
468
|
|
|
$results, |
469
|
|
|
$commandData |
470
|
|
|
); |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
protected function createCommandData(InputInterface $input, OutputInterface $output) |
474
|
|
|
{ |
475
|
|
|
$commandData = new CommandData( |
476
|
|
|
$this->annotationData, |
477
|
|
|
$input, |
478
|
|
|
$output |
479
|
|
|
); |
480
|
|
|
|
481
|
|
|
$commandData->setUseIOInterfaces( |
482
|
|
|
$this->usesOutputInterface, |
|
|
|
|
483
|
|
|
$this->usesInputInterface |
484
|
|
|
); |
485
|
|
|
|
486
|
|
|
return $commandData; |
487
|
|
|
} |
488
|
|
|
} |
489
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.