Completed
Push — master ( ba1614...930cbb )
by Greg
09:03 queued 06:27
created

AnnotatedCommand::explicitOptions()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 8.439
c 0
b 0
f 0
cc 5
eloc 16
nc 7
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 Symfony\Component\Console\Command\Command;
9
use Symfony\Component\Console\Input\InputArgument;
10
use Symfony\Component\Console\Input\InputInterface;
11
use Symfony\Component\Console\Input\InputOption;
12
use Symfony\Component\Console\Output\OutputInterface;
13
14
/**
15
 * AnnotatedCommands are created automatically by the
16
 * AnnotatedCommandFactory.  Each command method in a
17
 * command file will produce one AnnotatedCommand.  These
18
 * are then added to your Symfony Console Application object;
19
 * nothing else is needed.
20
 *
21
 * Optionally, though, you may extend AnnotatedCommand directly
22
 * to make a single command.  The usage pattern is the same
23
 * as for any other Symfony Console command, except that you may
24
 * omit the 'Confiure' method, and instead place your annotations
25
 * on the execute() method.
26
 *
27
 * @package Consolidation\AnnotatedCommand
28
 */
29
class AnnotatedCommand extends Command
30
{
31
    protected $commandCallback;
32
    protected $commandProcessor;
33
    protected $annotationData;
34
    protected $usesInputInterface;
35
    protected $usesOutputInterface;
36
    protected $returnType;
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 = new CommandInfo($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
        }
58
    }
59
60
    public function setCommandCallback($commandCallback)
61
    {
62
        $this->commandCallback = $commandCallback;
63
    }
64
65
    public function setCommandProcessor($commandProcessor)
66
    {
67
        $this->commandProcessor = $commandProcessor;
68
    }
69
70
    public function getCommandProcessor()
71
    {
72
        // If someone is using an AnnotatedCommand, and is NOT getting
73
        // it from an AnnotatedCommandFactory OR not correctly injecting
74
        // a command processor via setCommandProcessor() (ideally via the
75
        // DI container), then we'll just give each annotated command its
76
        // own command processor. This is not ideal; preferably, there would
77
        // only be one instance of the command processor in the application.
78
        if (!isset($this->commandProcessor)) {
79
            $this->commandProcessor = new CommandProcessor(new HookManager());
80
        }
81
        return $this->commandProcessor;
82
    }
83
84
    public function getReturnType()
85
    {
86
        return $this->returnType;
87
    }
88
89
    public function setReturnType($returnType)
90
    {
91
        $this->returnType = $returnType;
92
    }
93
94
    public function setAnnotationData($annotationData)
95
    {
96
        $this->annotationData = $annotationData;
97
    }
98
99
    public function setCommandInfo($commandInfo)
100
    {
101
        $this->setDescription($commandInfo->getDescription());
102
        $this->setHelp($commandInfo->getHelp());
103
        $this->setAliases($commandInfo->getAliases());
104
        $this->setAnnotationData($commandInfo->getAnnotationsForCommand());
105
        foreach ($commandInfo->getExampleUsages() as $usage => $description) {
106
            // Symfony Console does not support attaching a description to a usage
107
            $this->addUsage($usage);
108
        }
109
        $this->setCommandArguments($commandInfo);
110
        $this->setCommandOptions($commandInfo);
111
        $this->setReturnType($commandInfo->getReturnType());
112
    }
113
114
    protected function setCommandArguments($commandInfo)
115
    {
116
        $this->setUsesInputInterface($commandInfo);
117
        $this->setUsesOutputInterface($commandInfo);
118
        $this->setCommandArgumentsFromParameters($commandInfo);
119
    }
120
121
    /**
122
     * Check whether the first parameter is an InputInterface.
123
     */
124
    protected function checkUsesInputInterface($params)
125
    {
126
        $firstParam = reset($params);
127
        return $firstParam instanceof InputInterface;
128
    }
129
130
    /**
131
     * Determine whether this command wants to get its inputs
132
     * via an InputInterface or via its command parameters
133
     */
134
    protected function setUsesInputInterface($commandInfo)
135
    {
136
        $params = $commandInfo->getParameters();
137
        $this->usesInputInterface = $this->checkUsesInputInterface($params);
138
    }
139
140
    /**
141
     * Determine whether this command wants to send its output directly
142
     * to the provided OutputInterface, or whether it will returned
143
     * structured output to be processed by the command processor.
144
     */
145
    protected function setUsesOutputInterface($commandInfo)
146
    {
147
        $params = $commandInfo->getParameters();
148
        $index = $this->checkUsesInputInterface($params) ? 1 : 0;
149
        $this->usesOutputInterface =
150
            (count($params) > $index) &&
151
            ($params[$index] instanceof OutputInterface);
152
    }
153
154
    protected function setCommandArgumentsFromParameters($commandInfo)
155
    {
156
        $args = $commandInfo->arguments()->getValues();
157
        foreach ($args as $name => $defaultValue) {
158
            $description = $commandInfo->arguments()->getDescription($name);
159
            $hasDefault = $commandInfo->arguments()->hasDefault($name);
160
            $parameterMode = $this->getCommandArgumentMode($hasDefault, $defaultValue);
161
            $this->addArgument($name, $parameterMode, $description, $defaultValue);
162
        }
163
    }
164
165
    protected function getCommandArgumentMode($hasDefault, $defaultValue)
166
    {
167
        if (!$hasDefault) {
168
            return InputArgument::REQUIRED;
169
        }
170
        if (is_array($defaultValue)) {
171
            return InputArgument::IS_ARRAY;
172
        }
173
        return InputArgument::OPTIONAL;
174
    }
175
176
    protected function setCommandOptions($commandInfo)
177
    {
178
        $automaticOptions = $this->automaticOptions($commandInfo);
179
        $explicitOptions = $this->explicitOptions($commandInfo);
180
181
        $this->addOptions($explicitOptions + $automaticOptions, $automaticOptions);
182
    }
183
184
    protected function addOptions($inputOptions, $automaticOptions)
185
    {
186
        foreach ($inputOptions as $name => $inputOption) {
187
            $default = $inputOption->getDefault();
188
            $description = $inputOption->getDescription();
189
190
            if (empty($description) && isset($automaticOptions[$name])) {
191
                $description = $automaticOptions[$name]->getDescription();
192
            }
193
194
            // Recover the 'mode' value, because Symfony is stubborn
195
            $mode = 0;
196
            if ($inputOption->isValueRequired()) {
197
                $mode |= InputOption::VALUE_REQUIRED;
198
            }
199
            if ($inputOption->isValueOptional()) {
200
                $mode |= InputOption::VALUE_OPTIONAL;
201
            }
202
            if ($inputOption->isArray()) {
203
                $mode |= InputOption::VALUE_IS_ARRAY;
204
            }
205
            if (!$mode) {
206
                $mode = InputOption::VALUE_NONE;
207
                $default = null;
208
            }
209
210
            // Add the option; note that Symfony doesn't have a convenient
211
            // method to do this that takes an InputOption
212
            $this->addOption(
213
                $inputOption->getName(),
214
                $inputOption->getShortcut(),
215
                $mode,
216
                $description,
217
                $default
218
            );
219
        }
220
    }
221
222
    /**
223
     * Get the options that are explicitly defined, e.g. via
224
     * @option annotations, or via $options = ['someoption' => 'defaultvalue']
225
     * in the command method parameter list.
226
     *
227
     * @return InputOption[]
228
     */
229
    protected function explicitOptions($commandInfo)
230
    {
231
        $explicitOptions = [];
232
233
        $opts = $commandInfo->options()->getValues();
234
        foreach ($opts as $name => $defaultValue) {
235
            $description = $commandInfo->options()->getDescription($name);
236
237
            $fullName = $name;
238
            $shortcut = '';
239
            if (strpos($name, '|')) {
240
                list($fullName, $shortcut) = explode('|', $name, 2);
241
            }
242
243
            if (is_bool($defaultValue)) {
244
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_NONE, $description);
245
            } elseif ($defaultValue === InputOption::VALUE_REQUIRED) {
246
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_REQUIRED, $description);
247
            } else {
248
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $defaultValue);
249
            }
250
        }
251
252
        return $explicitOptions;
253
    }
254
255
    /**
256
     * Get the options that are implied by annotations, e.g. @fields implies
257
     * that there should be a --fields and a --format option.
258
     *
259
     * @return InputOption[]
260
     */
261
    protected function automaticOptions($commandInfo)
262
    {
263
        $formatManager = $this->getCommandProcessor()->formatterManager();
264
        if ($formatManager) {
265
            $formatterOptions = new FormatterOptions($this->annotationData->getArrayCopy());
266
            $dataType = $commandInfo->getReturnType();
267
            $automaticOptions = $formatManager->automaticOptions($formatterOptions, $dataType);
268
            return $automaticOptions;
269
        }
270
        return [];
271
    }
272
273
    protected function getArgsWithPassThrough($input)
274
    {
275
        $args = $input->getArguments();
276
277
        // When called via the Application, the first argument
278
        // will be the command name. The Application alters the
279
        // input definition to match, adding a 'command' argument
280
        // to the beginning.
281
        array_shift($args);
282
        if ($input instanceof PassThroughArgsInput) {
283
            return $this->appendPassThroughArgs($input, $args);
284
        }
285
        return $args;
286
    }
287
288
    protected function getArgsAndOptions($input)
289
    {
290
        if (!$input) {
291
            return [];
292
        }
293
        // Get passthrough args, and add the options on the end.
294
        $args = $this->getArgsWithPassThrough($input);
295
        $args[] = $input->getOptions();
296
        return $args;
297
    }
298
299
    protected function appendPassThroughArgs($input, $args)
300
    {
301
        $passThrough = $input->getPassThroughArgs();
302
        $definition = $this->getDefinition();
303
        $argumentDefinitions = $definition->getArguments();
304
        $lastParameter = end($argumentDefinitions);
305
        if ($lastParameter && $lastParameter->isArray()) {
306
            $args[$lastParameter->getName()] = array_merge($args[$lastParameter->getName()], $passThrough);
307
        } else {
308
            $args[$lastParameter->getName()] = implode(' ', $passThrough);
309
        }
310
        return $args;
311
    }
312
313
    /**
314
     * Returns all of the hook names that may be called for this command.
315
     *
316
     * @return array
317
     */
318
    protected function getNames()
319
    {
320
        return array_filter(
321
            array_merge(
322
                $this->getNamesUsingCommands(),
323
                [HookManager::getClassNameFromCallback($this->commandCallback)]
324
            )
325
        );
326
    }
327
328
    protected function getNamesUsingCommands()
329
    {
330
        return array_merge(
331
            [$this->getName()],
332
            $this->getAliases()
333
        );
334
    }
335
336
    /**
337
     * {@inheritdoc}
338
     */
339
    protected function interact(InputInterface $input, OutputInterface $output)
340
    {
341
        $this->getCommandProcessor()->interact(
342
            $input,
343
            $output,
344
            $this->getNames(),
345
            $this->annotationData
346
        );
347
    }
348
349
    /**
350
     * {@inheritdoc}
351
     */
352
    protected function execute(InputInterface $input, OutputInterface $output)
353
    {
354
        // Get passthrough args, and add the options on the end.
355
        $args = $this->getArgsAndOptions($input);
356
357
        if ($this->usesInputInterface) {
358
            array_unshift($args, $input);
359
        }
360
        if ($this->usesOutputInterface) {
361
            array_unshift($args, $output);
362
        }
363
364
        // Validate, run, process, alter, handle results.
365
        return $this->getCommandProcessor()->process(
366
            $output,
367
            $this->getNames(),
368
            $this->commandCallback,
369
            $this->annotationData,
370
            $args
371
        );
372
    }
373
374
    public function processResults(InputInterface $input, OutputInterface $output, $results)
375
    {
376
        $commandProcessor = $this->getCommandProcessor();
377
        $names = $this->getNames();
378
        $args = $this->getArgsAndOptions($input);
379
        $results = $commandProcessor->processResults(
380
            $names,
381
            $results,
382
            $args,
383
            $this->annotationData
384
        );
385
        $options = end($args);
386
        return $commandProcessor->handleResults(
387
            $output,
388
            $names,
389
            $results,
390
            $this->annotationData,
391
            $options
392
        );
393
    }
394
}
395