Completed
Pull Request — master (#65)
by
unknown
04:30
created

AnnotatedCommand::setUsesOutputInterface()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 9
rs 9.6666
cc 3
eloc 7
nc 4
nop 1
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 Symfony\Component\Console\Command\Command;
10
use Symfony\Component\Console\Input\InputArgument;
11
use Symfony\Component\Console\Input\InputInterface;
12
use Symfony\Component\Console\Input\InputOption;
13
use Symfony\Component\Console\Output\OutputInterface;
14
15
/**
16
 * AnnotatedCommands are created automatically by the
17
 * AnnotatedCommandFactory.  Each command method in a
18
 * command file will produce one AnnotatedCommand.  These
19
 * are then added to your Symfony Console Application object;
20
 * nothing else is needed.
21
 *
22
 * Optionally, though, you may extend AnnotatedCommand directly
23
 * to make a single command.  The usage pattern is the same
24
 * as for any other Symfony Console command, except that you may
25
 * omit the 'Confiure' method, and instead place your annotations
26
 * on the execute() method.
27
 *
28
 * @package Consolidation\AnnotatedCommand
29
 */
30
class AnnotatedCommand extends Command implements HelpDocumentAlter
31
{
32
    protected $calls = [];
33
    protected $commandCallback;
34
    protected $commandProcessor;
35
    protected $annotationData;
36
    protected $examples = [];
37
    protected $topics = [];
38
    protected $usesInputInterface;
39
    protected $usesOutputInterface;
40
    protected $returnType;
41
42
    public function __construct($name = null)
43
    {
44
        $commandInfo = false;
45
46
        // If this is a subclass of AnnotatedCommand, check to see
47
        // if the 'execute' method is annotated.  We could do this
48
        // unconditionally; it is a performance optimization to skip
49
        // checking the annotations if $this is an instance of
50
        // AnnotatedCommand.  Alternately, we break out a new subclass.
51
        // The command factory instantiates the subclass.
52
        if (get_class($this) != 'Consolidation\AnnotatedCommand\AnnotatedCommand') {
53
            $commandInfo = new CommandInfo($this, 'execute');
54
            if (!isset($name)) {
55
                $name = $commandInfo->getName();
56
            }
57
        }
58
        parent::__construct($name);
59
        if ($commandInfo && $commandInfo->hasAnnotation('command')) {
60
            $this->setCommandInfo($commandInfo);
61
            $this->setCommandOptions($commandInfo);
62
        }
63
    }
64
65
    public function setCommandCallback($commandCallback)
66
    {
67
        $this->commandCallback = $commandCallback;
68
        return $this;
69
    }
70
71
    public function setCommandProcessor($commandProcessor)
72
    {
73
        $this->commandProcessor = $commandProcessor;
74
        return $this;
75
    }
76
77
    public function commandProcessor()
78
    {
79
        // If someone is using an AnnotatedCommand, and is NOT getting
80
        // it from an AnnotatedCommandFactory OR not correctly injecting
81
        // a command processor via setCommandProcessor() (ideally via the
82
        // DI container), then we'll just give each annotated command its
83
        // own command processor. This is not ideal; preferably, there would
84
        // only be one instance of the command processor in the application.
85
        if (!isset($this->commandProcessor)) {
86
            $this->commandProcessor = new CommandProcessor(new HookManager());
87
        }
88
        return $this->commandProcessor;
89
    }
90
91
    public function getReturnType()
92
    {
93
        return $this->returnType;
94
    }
95
96
    public function setReturnType($returnType)
97
    {
98
        $this->returnType = $returnType;
99
        return $this;
100
    }
101
102
    public function getAnnotationData()
103
    {
104
        return $this->annotationData;
105
    }
106
107
    public function setAnnotationData($annotationData)
108
    {
109
        $this->annotationData = $annotationData;
110
        return $this;
111
    }
112
113
    public function getTopics()
114
    {
115
        return $this->topics;
116
    }
117
118
    public function setTopics($topics)
119
    {
120
        $this->topics = $topics;
121
        return $this;
122
    }
123
124
    public function getCalls()
125
    {
126
        return $this->calls;
127
    }
128
129
    public function setCalls($calls)
130
    {
131
        $this->calls = $calls;
132
        return $this;
133
    }
134
135
    public function setCommandInfo($commandInfo)
136
    {
137
        $this->setCalls($commandInfo->getCalls());
138
        $this->setDescription($commandInfo->getDescription());
139
        $this->setHelp($commandInfo->getHelp());
140
        $this->setAliases($commandInfo->getAliases());
141
        $this->setAnnotationData($commandInfo->getAnnotations());
142
        $this->setTopics($commandInfo->getTopics());
143
        foreach ($commandInfo->getExampleUsages() as $usage => $description) {
144
            $this->addUsageOrExample($usage, $description);
145
        }
146
        $this->setCommandArguments($commandInfo);
147
        $this->setReturnType($commandInfo->getReturnType());
148
        return $this;
149
    }
150
151
    protected function addUsageOrExample($usage, $description)
152
    {
153
        $this->addUsage($usage);
154
        if (!empty($description)) {
155
            $this->examples[$usage] = $description;
156
        }
157
    }
158
159
    public function helpAlter(\DomDocument $originalDom)
160
    {
161
        $dom = new \DOMDocument('1.0', 'UTF-8');
162
        $dom->appendChild($commandXML = $dom->createElement('command'));
163
        $commandXML->setAttribute('id', $this->getName());
164
        $commandXML->setAttribute('name', $this->getName());
165
166
        // Get the original <command> element and its top-level elements.
167
        $originalCommandXML = $this->getSingleElementByTagName($dom, $originalDom, 'command');
168
        $originalUsagesXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'usages');
169
        $originalDescriptionXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'description');
170
        $originalHelpXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'help');
171
        $originalArgumentsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'arguments');
172
        $originalOptionsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'options');
173
174
        // Keep only the first of the <usage> elements
175
        $newUsagesXML = $dom->createElement('usages');
176
        $firstUsageXML = $this->getSingleElementByTagName($dom, $originalUsagesXML, 'usage');
177
        $newUsagesXML->appendChild($firstUsageXML);
178
179
        // Create our own <example> elements
180
        $newExamplesXML = $dom->createElement('examples');
181
        foreach ($this->examples as $usage => $description) {
182
            $newExamplesXML->appendChild($exampleXML = $dom->createElement('example'));
183
            $exampleXML->appendChild($usageXML = $dom->createElement('usage', $usage));
184
            $exampleXML->appendChild($descriptionXML = $dom->createElement('description', $description));
185
        }
186
187
        // Create our own <alias> elements
188
        $newAliasesXML = $dom->createElement('aliases');
189
        foreach ($this->getAliases() as $alias) {
190
            $newAliasesXML->appendChild($dom->createElement('alias', $alias));
191
        }
192
193
        // Create our own <topic> elements
194
        $newTopicsXML = $dom->createElement('topics');
195
        foreach ($this->getTopics() as $topic) {
196
            $newTopicsXML->appendChild($topicXML = $dom->createElement('topic', $topic));
197
        }
198
199
        // Place the different elements into the <command> element in the desired order
200
        $commandXML->appendChild($newUsagesXML);
201
        $commandXML->appendChild($newExamplesXML);
202
        $commandXML->appendChild($originalDescriptionXML);
203
        $commandXML->appendChild($originalArgumentsXML);
204
        $commandXML->appendChild($originalOptionsXML);
205
        $commandXML->appendChild($originalHelpXML);
206
        $commandXML->appendChild($newAliasesXML);
207
        $commandXML->appendChild($newTopicsXML);
208
209
        return $dom;
210
    }
211
212
    protected function getSingleElementByTagName($dom, $parent, $tagName)
213
    {
214
        // There should always be exactly one '<command>' element.
215
        $elements = $parent->getElementsByTagName($tagName);
216
        $result = $elements->item(0);
217
218
        $result = $dom->importNode($result, true);
219
220
        return $result;
221
    }
222
223
    protected function setCommandArguments($commandInfo)
224
    {
225
        $this->setUsesInputInterface($commandInfo);
226
        $this->setUsesOutputInterface($commandInfo);
227
        $this->setCommandArgumentsFromParameters($commandInfo);
228
        return $this;
229
    }
230
231
    /**
232
     * Check whether the first parameter is an InputInterface.
233
     */
234
    protected function checkUsesInputInterface($params)
235
    {
236
        $firstParam = reset($params);
237
        return $firstParam instanceof InputInterface;
238
    }
239
240
    /**
241
     * Determine whether this command wants to get its inputs
242
     * via an InputInterface or via its command parameters
243
     */
244
    protected function setUsesInputInterface($commandInfo)
245
    {
246
        $params = $commandInfo->getParameters();
247
        $this->usesInputInterface = $this->checkUsesInputInterface($params);
248
        return $this;
249
    }
250
251
    /**
252
     * Determine whether this command wants to send its output directly
253
     * to the provided OutputInterface, or whether it will returned
254
     * structured output to be processed by the command processor.
255
     */
256
    protected function setUsesOutputInterface($commandInfo)
257
    {
258
        $params = $commandInfo->getParameters();
259
        $index = $this->checkUsesInputInterface($params) ? 1 : 0;
260
        $this->usesOutputInterface =
261
            (count($params) > $index) &&
262
            ($params[$index] instanceof OutputInterface);
263
        return $this;
264
    }
265
266
    protected function setCommandArgumentsFromParameters($commandInfo)
267
    {
268
        $args = $commandInfo->arguments()->getValues();
269
        foreach ($args as $name => $defaultValue) {
270
            $description = $commandInfo->arguments()->getDescription($name);
271
            $hasDefault = $commandInfo->arguments()->hasDefault($name);
272
            $parameterMode = $this->getCommandArgumentMode($hasDefault, $defaultValue);
273
            $this->addArgument($name, $parameterMode, $description, $defaultValue);
274
        }
275
        return $this;
276
    }
277
278
    protected function getCommandArgumentMode($hasDefault, $defaultValue)
279
    {
280
        if (!$hasDefault) {
281
            return InputArgument::REQUIRED;
282
        }
283
        if (is_array($defaultValue)) {
284
            return InputArgument::IS_ARRAY;
285
        }
286
        return InputArgument::OPTIONAL;
287
    }
288
289
    public function setCommandOptions($commandInfo, $automaticOptions = [])
290
    {
291
        $inputOptions = $commandInfo->inputOptions();
292
293
        $this->addOptions($inputOptions + $automaticOptions, $automaticOptions);
294
        return $this;
295
    }
296
297
    public function addOptions($inputOptions, $automaticOptions = [])
298
    {
299
        foreach ($inputOptions as $name => $inputOption) {
300
            $description = $inputOption->getDescription();
301
302
            if (empty($description) && isset($automaticOptions[$name])) {
303
                $description = $automaticOptions[$name]->getDescription();
304
                $inputOption = static::inputOptionSetDescription($inputOption, $description);
305
            }
306
            $this->getDefinition()->addOption($inputOption);
307
        }
308
    }
309
310
    protected static function inputOptionSetDescription($inputOption, $description)
311
    {
312
        // Recover the 'mode' value, because Symfony is stubborn
313
        $mode = 0;
314
        if ($inputOption->isValueRequired()) {
315
            $mode |= InputOption::VALUE_REQUIRED;
316
        }
317
        if ($inputOption->isValueOptional()) {
318
            $mode |= InputOption::VALUE_OPTIONAL;
319
        }
320
        if ($inputOption->isArray()) {
321
            $mode |= InputOption::VALUE_IS_ARRAY;
322
        }
323
        if (!$mode) {
324
            $mode = InputOption::VALUE_NONE;
325
        }
326
327
        $inputOption = new InputOption(
328
            $inputOption->getName(),
329
            $inputOption->getShortcut(),
330
            $mode,
331
            $description,
332
            $inputOption->getDefault()
333
        );
334
        return $inputOption;
335
    }
336
337
    /**
338
     * Returns all of the hook names that may be called for this command.
339
     *
340
     * @return array
341
     */
342
    public function getNames()
343
    {
344
        return HookManager::getNames($this, $this->commandCallback);
345
    }
346
347
    /**
348
     * Add any options to this command that are defined by hook implementations
349
     */
350
    public function optionsHook()
351
    {
352
        $this->commandProcessor()->optionsHook(
353
            $this,
354
            $this->getNames(),
355
            $this->annotationData
356
        );
357
    }
358
359
    public function optionsHookForHookAnnotations($commandInfoList)
360
    {
361
        foreach ($commandInfoList as $commandInfo) {
362
            $inputOptions = $commandInfo->inputOptions();
363
            $this->addOptions($inputOptions);
364
            foreach ($commandInfo->getExampleUsages() as $usage => $description) {
365
                if (!in_array($usage, $this->getUsages())) {
366
                    $this->addUsageOrExample($usage, $description);
367
                }
368
            }
369
        }
370
    }
371
372
    /**
373
     * {@inheritdoc}
374
     */
375
    protected function interact(InputInterface $input, OutputInterface $output)
376
    {
377
        $this->commandProcessor()->interact(
378
            $input,
379
            $output,
380
            $this->getNames(),
381
            $this->annotationData
382
        );
383
    }
384
385
    protected function initialize(InputInterface $input, OutputInterface $output)
386
    {
387
        // Allow the hook manager a chance to provide configuration values,
388
        // if there are any registered hooks to do that.
389
        $this->commandProcessor()->initializeHook($input, $this->getNames(), $this->annotationData);
390
    }
391
392
    protected function executeCallsCommands() {
393
        if ($this->getCalls()) {
394
            foreach ($this->getCalls as $command_name) {
0 ignored issues
show
Bug introduced by
The property getCalls does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
Unused Code introduced by
This foreach statement is empty and can be removed.

This check looks for foreach loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
395
                // @todo validate that $command_name exists in this application.
396
                // @todo Execute command.
397
                // @todo On failure, return exit code of called command.
398
            }
399
        }
400
    }
401
402
    /**
403
     * {@inheritdoc}
404
     */
405
    protected function execute(InputInterface $input, OutputInterface $output)
406
    {
407
        // @todo Call $this->executeCallsCommands();
0 ignored issues
show
Unused Code Comprehensibility introduced by
46% 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...
408
409
        // Validate, run, process, alter, handle results.
410
        return $this->commandProcessor()->process(
411
            $output,
412
            $this->getNames(),
413
            $this->commandCallback,
414
            $this->createCommandData($input, $output)
415
        );
416
    }
417
418
    /**
419
     * This function is available for use by a class that may
420
     * wish to extend this class rather than use annotations to
421
     * define commands. Using this technique does allow for the
422
     * use of annotations to define hooks.
423
     */
424
    public function processResults(InputInterface $input, OutputInterface $output, $results)
425
    {
426
        $commandData = $this->createCommandData($input, $output);
427
        $commandProcessor = $this->commandProcessor();
428
        $names = $this->getNames();
429
        $results = $commandProcessor->processResults(
430
            $names,
431
            $results,
432
            $commandData
433
        );
434
        return $commandProcessor->handleResults(
435
            $output,
436
            $names,
437
            $results,
438
            $commandData
439
        );
440
    }
441
442
    protected function createCommandData(InputInterface $input, OutputInterface $output)
443
    {
444
        $commandData = new CommandData(
445
            $this->annotationData,
446
            $input,
447
            $output
448
        );
449
450
        $commandData->setUseIOInterfaces(
451
            $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...
452
            $this->usesInputInterface
453
        );
454
455
        return $commandData;
456
    }
457
}
458