Completed
Push — master ( 7c97c4...f27fd4 )
by Greg
02:25
created

AnnotatedCommand::getExampleUsages()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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