Completed
Pull Request — master (#65)
by
unknown
02:33
created

AnnotatedCommand::getNames()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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');
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
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.");
0 ignored issues
show
Unused Code Comprehensibility introduced by
86% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
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.");
0 ignored issues
show
Unused Code Comprehensibility introduced by
86% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
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,
0 ignored issues
show
Documentation introduced by
$this->usesOutputInterface is of type boolean, but the function expects a object<Consolidation\AnnotatedCommand\booean>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
483
            $this->usesInputInterface
484
        );
485
486
        return $commandData;
487
    }
488
}
489