Completed
Pull Request — master (#179)
by Greg
01:48
created

AnnotatedCommand::setUsesInputInterface()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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