Completed
Push — master ( c497f7...2066be )
by Greg
02:05
created

AnnotatedCommand::getTopics()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
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 = new CommandInfo($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
    protected function addUsageOrExample($usage, $description)
139
    {
140
        $this->addUsage($usage);
141
        if (!empty($description)) {
142
            $this->examples[$usage] = $description;
143
        }
144
    }
145
146
    public function helpAlter(\DomDocument $originalDom)
147
    {
148
        $dom = new \DOMDocument('1.0', 'UTF-8');
149
        $dom->appendChild($commandXML = $dom->createElement('command'));
150
        $commandXML->setAttribute('id', $this->getName());
151
        $commandXML->setAttribute('name', $this->getName());
152
153
        // Get the original <command> element and its top-level elements.
154
        $originalCommandXML = $this->getSingleElementByTagName($dom, $originalDom, 'command');
155
        $originalUsagesXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'usages');
156
        $originalDescriptionXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'description');
157
        $originalHelpXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'help');
158
        $originalArgumentsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'arguments');
159
        $originalOptionsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'options');
160
161
        // Keep only the first of the <usage> elements
162
        $newUsagesXML = $dom->createElement('usages');
163
        $firstUsageXML = $this->getSingleElementByTagName($dom, $originalUsagesXML, 'usage');
164
        $newUsagesXML->appendChild($firstUsageXML);
165
166
        // Create our own <example> elements
167
        $newExamplesXML = $dom->createElement('examples');
168
        foreach ($this->examples as $usage => $description) {
169
            $newExamplesXML->appendChild($exampleXML = $dom->createElement('example'));
170
            $exampleXML->appendChild($usageXML = $dom->createElement('usage', $usage));
171
            $exampleXML->appendChild($descriptionXML = $dom->createElement('description', $description));
172
        }
173
174
        // Create our own <alias> elements
175
        $newAliasesXML = $dom->createElement('aliases');
176
        foreach ($this->getAliases() as $alias) {
177
            $newAliasesXML->appendChild($dom->createElement('alias', $alias));
178
        }
179
180
        // Create our own <topic> elements
181
        $newTopicsXML = $dom->createElement('topics');
182
        foreach ($this->getTopics() as $topic => $description) {
183
            $newTopicsXML->appendChild($topicXML = $dom->createElement('topic'));
184
            $topicXML->appendChild($nameXML = $dom->createElement('name', $topic));
185
            $topicXML->appendChild($descriptionXML = $dom->createElement('description', $description));
186
        }
187
188
        // Place the different elements into the <command> element in the desired order
189
        $commandXML->appendChild($newUsagesXML);
190
        $commandXML->appendChild($newExamplesXML);
191
        $commandXML->appendChild($originalDescriptionXML);
192
        $commandXML->appendChild($originalArgumentsXML);
193
        $commandXML->appendChild($originalOptionsXML);
194
        $commandXML->appendChild($originalHelpXML);
195
        $commandXML->appendChild($newAliasesXML);
196
        $commandXML->appendChild($newTopicsXML);
197
198
        return $dom;
199
    }
200
201
    protected function getSingleElementByTagName($dom, $parent, $tagName)
202
    {
203
        // There should always be exactly one '<command>' element.
204
        $elements = $parent->getElementsByTagName($tagName);
205
        $result = $elements->item(0);
206
207
        $result = $dom->importNode($result, true);
208
209
        return $result;
210
    }
211
212
    protected function setCommandArguments($commandInfo)
213
    {
214
        $this->setUsesInputInterface($commandInfo);
215
        $this->setUsesOutputInterface($commandInfo);
216
        $this->setCommandArgumentsFromParameters($commandInfo);
217
        return $this;
218
    }
219
220
    /**
221
     * Check whether the first parameter is an InputInterface.
222
     */
223
    protected function checkUsesInputInterface($params)
224
    {
225
        $firstParam = reset($params);
226
        return $firstParam instanceof InputInterface;
227
    }
228
229
    /**
230
     * Determine whether this command wants to get its inputs
231
     * via an InputInterface or via its command parameters
232
     */
233
    protected function setUsesInputInterface($commandInfo)
234
    {
235
        $params = $commandInfo->getParameters();
236
        $this->usesInputInterface = $this->checkUsesInputInterface($params);
237
        return $this;
238
    }
239
240
    /**
241
     * Determine whether this command wants to send its output directly
242
     * to the provided OutputInterface, or whether it will returned
243
     * structured output to be processed by the command processor.
244
     */
245
    protected function setUsesOutputInterface($commandInfo)
246
    {
247
        $params = $commandInfo->getParameters();
248
        $index = $this->checkUsesInputInterface($params) ? 1 : 0;
249
        $this->usesOutputInterface =
250
            (count($params) > $index) &&
251
            ($params[$index] instanceof OutputInterface);
252
        return $this;
253
    }
254
255
    protected function setCommandArgumentsFromParameters($commandInfo)
256
    {
257
        $args = $commandInfo->arguments()->getValues();
258
        foreach ($args as $name => $defaultValue) {
259
            $description = $commandInfo->arguments()->getDescription($name);
260
            $hasDefault = $commandInfo->arguments()->hasDefault($name);
261
            $parameterMode = $this->getCommandArgumentMode($hasDefault, $defaultValue);
262
            $this->addArgument($name, $parameterMode, $description, $defaultValue);
263
        }
264
        return $this;
265
    }
266
267
    protected function getCommandArgumentMode($hasDefault, $defaultValue)
268
    {
269
        if (!$hasDefault) {
270
            return InputArgument::REQUIRED;
271
        }
272
        if (is_array($defaultValue)) {
273
            return InputArgument::IS_ARRAY;
274
        }
275
        return InputArgument::OPTIONAL;
276
    }
277
278
    public function setCommandOptions($commandInfo, $automaticOptions = [])
279
    {
280
        $inputOptions = $commandInfo->inputOptions();
281
282
        $this->addOptions($inputOptions + $automaticOptions, $automaticOptions);
283
        return $this;
284
    }
285
286
    public function addOptions($inputOptions, $automaticOptions = [])
287
    {
288
        foreach ($inputOptions as $name => $inputOption) {
289
            $description = $inputOption->getDescription();
290
291
            if (empty($description) && isset($automaticOptions[$name])) {
292
                $description = $automaticOptions[$name]->getDescription();
293
                $inputOption = static::inputOptionSetDescription($inputOption, $description);
294
            }
295
            $this->getDefinition()->addOption($inputOption);
296
        }
297
    }
298
299
    protected static function inputOptionSetDescription($inputOption, $description)
300
    {
301
        // Recover the 'mode' value, because Symfony is stubborn
302
        $mode = 0;
303
        if ($inputOption->isValueRequired()) {
304
            $mode |= InputOption::VALUE_REQUIRED;
305
        }
306
        if ($inputOption->isValueOptional()) {
307
            $mode |= InputOption::VALUE_OPTIONAL;
308
        }
309
        if ($inputOption->isArray()) {
310
            $mode |= InputOption::VALUE_IS_ARRAY;
311
        }
312
        if (!$mode) {
313
            $mode = InputOption::VALUE_NONE;
314
        }
315
316
        $inputOption = new InputOption(
317
            $inputOption->getName(),
318
            $inputOption->getShortcut(),
319
            $mode,
320
            $description,
321
            $inputOption->getDefault()
322
        );
323
        return $inputOption;
324
    }
325
326
    /**
327
     * Returns all of the hook names that may be called for this command.
328
     *
329
     * @return array
330
     */
331
    public function getNames()
332
    {
333
        return HookManager::getNames($this, $this->commandCallback);
334
    }
335
336
    /**
337
     * Add any options to this command that are defined by hook implementations
338
     */
339
    public function optionsHook()
340
    {
341
        $this->commandProcessor()->optionsHook(
342
            $this,
343
            $this->getNames(),
344
            $this->annotationData
345
        );
346
    }
347
348
    public function optionsHookForHookAnnotations($commandInfoList)
349
    {
350
        foreach ($commandInfoList as $commandInfo) {
351
            $inputOptions = $commandInfo->inputOptions();
352
            $this->addOptions($inputOptions);
353
            foreach ($commandInfo->getExampleUsages() as $usage => $description) {
354
                if (!in_array($usage, $this->getUsages())) {
355
                    $this->addUsageOrExample($usage, $description);
356
                }
357
            }
358
        }
359
    }
360
361
    /**
362
     * {@inheritdoc}
363
     */
364
    protected function interact(InputInterface $input, OutputInterface $output)
365
    {
366
        $this->commandProcessor()->interact(
367
            $input,
368
            $output,
369
            $this->getNames(),
370
            $this->annotationData
371
        );
372
    }
373
374
    protected function initialize(InputInterface $input, OutputInterface $output)
375
    {
376
        // Allow the hook manager a chance to provide configuration values,
377
        // if there are any registered hooks to do that.
378
        $this->commandProcessor()->initializeHook($input, $this->getNames(), $this->annotationData);
379
    }
380
381
    /**
382
     * {@inheritdoc}
383
     */
384
    protected function execute(InputInterface $input, OutputInterface $output)
385
    {
386
        // Validate, run, process, alter, handle results.
387
        return $this->commandProcessor()->process(
388
            $output,
389
            $this->getNames(),
390
            $this->commandCallback,
391
            $this->createCommandData($input, $output)
392
        );
393
    }
394
395
    /**
396
     * This function is available for use by a class that may
397
     * wish to extend this class rather than use annotations to
398
     * define commands. Using this technique does allow for the
399
     * use of annotations to define hooks.
400
     */
401
    public function processResults(InputInterface $input, OutputInterface $output, $results)
402
    {
403
        $commandData = $this->createCommandData($input, $output);
404
        $commandProcessor = $this->commandProcessor();
405
        $names = $this->getNames();
406
        $results = $commandProcessor->processResults(
407
            $names,
408
            $results,
409
            $commandData
410
        );
411
        return $commandProcessor->handleResults(
412
            $output,
413
            $names,
414
            $results,
415
            $commandData
416
        );
417
    }
418
419
    protected function createCommandData(InputInterface $input, OutputInterface $output)
420
    {
421
        $commandData = new CommandData(
422
            $this->annotationData,
423
            $input,
424
            $output
425
        );
426
427
        $commandData->setUseIOInterfaces(
428
            $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...
429
            $this->usesInputInterface
430
        );
431
432
        return $commandData;
433
    }
434
}
435