Completed
Push — master ( 47d5e6...daebf4 )
by Greg
11s
created

AnnotatedCommand::setUsesInputInterface()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
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\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
            $this->setCommandOptions($commandInfo);
58
        }
59
    }
60
61
    public function setCommandCallback($commandCallback)
62
    {
63
        $this->commandCallback = $commandCallback;
64
    }
65
66
    public function setCommandProcessor($commandProcessor)
67
    {
68
        $this->commandProcessor = $commandProcessor;
69
    }
70
71
    public function commandProcessor()
72
    {
73
        // If someone is using an AnnotatedCommand, and is NOT getting
74
        // it from an AnnotatedCommandFactory OR not correctly injecting
75
        // a command processor via setCommandProcessor() (ideally via the
76
        // DI container), then we'll just give each annotated command its
77
        // own command processor. This is not ideal; preferably, there would
78
        // only be one instance of the command processor in the application.
79
        if (!isset($this->commandProcessor)) {
80
            $this->commandProcessor = new CommandProcessor(new HookManager());
81
        }
82
        return $this->commandProcessor;
83
    }
84
85
    public function getReturnType()
86
    {
87
        return $this->returnType;
88
    }
89
90
    public function setReturnType($returnType)
91
    {
92
        $this->returnType = $returnType;
93
    }
94
95
    public function setAnnotationData($annotationData)
96
    {
97
        $this->annotationData = $annotationData;
98
    }
99
100
    public function setCommandInfo($commandInfo)
101
    {
102
        $this->setDescription($commandInfo->getDescription());
103
        $this->setHelp($commandInfo->getHelp());
104
        $this->setAliases($commandInfo->getAliases());
105
        $this->setAnnotationData($commandInfo->getAnnotationsForCommand());
106
        foreach ($commandInfo->getExampleUsages() as $usage => $description) {
107
            // Symfony Console does not support attaching a description to a usage
108
            $this->addUsage($usage);
109
        }
110
        $this->setCommandArguments($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
    public function setCommandOptions($commandInfo, $automaticOptions = [])
177
    {
178
        $explicitOptions = $this->explicitOptions($commandInfo);
179
180
        $this->addOptions($explicitOptions + $automaticOptions, $automaticOptions);
181
    }
182
183
    protected function addOptions($inputOptions, $automaticOptions)
184
    {
185
        foreach ($inputOptions as $name => $inputOption) {
186
            $default = $inputOption->getDefault();
187
            $description = $inputOption->getDescription();
188
189
            if (empty($description) && isset($automaticOptions[$name])) {
190
                $description = $automaticOptions[$name]->getDescription();
191
            }
192
193
            // Recover the 'mode' value, because Symfony is stubborn
194
            $mode = 0;
195
            if ($inputOption->isValueRequired()) {
196
                $mode |= InputOption::VALUE_REQUIRED;
197
            }
198
            if ($inputOption->isValueOptional()) {
199
                $mode |= InputOption::VALUE_OPTIONAL;
200
            }
201
            if ($inputOption->isArray()) {
202
                $mode |= InputOption::VALUE_IS_ARRAY;
203
            }
204
            if (!$mode) {
205
                $mode = InputOption::VALUE_NONE;
206
                $default = null;
207
            }
208
209
            // Add the option; note that Symfony doesn't have a convenient
210
            // method to do this that takes an InputOption
211
            $this->addOption(
212
                $inputOption->getName(),
213
                $inputOption->getShortcut(),
214
                $mode,
215
                $description,
216
                $default
217
            );
218
        }
219
    }
220
221
    /**
222
     * Get the options that are explicitly defined, e.g. via
223
     * @option annotations, or via $options = ['someoption' => 'defaultvalue']
224
     * in the command method parameter list.
225
     *
226
     * @return InputOption[]
227
     */
228
    protected function explicitOptions($commandInfo)
229
    {
230
        $explicitOptions = [];
231
232
        $opts = $commandInfo->options()->getValues();
233
        foreach ($opts as $name => $defaultValue) {
234
            $description = $commandInfo->options()->getDescription($name);
235
236
            $fullName = $name;
237
            $shortcut = '';
238
            if (strpos($name, '|')) {
239
                list($fullName, $shortcut) = explode('|', $name, 2);
240
            }
241
242
            if (is_bool($defaultValue)) {
243
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_NONE, $description);
244
            } elseif ($defaultValue === InputOption::VALUE_REQUIRED) {
245
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_REQUIRED, $description);
246
            } else {
247
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $defaultValue);
248
            }
249
        }
250
251
        return $explicitOptions;
252
    }
253
254
    protected function getArgsWithPassThrough($input)
255
    {
256
        $args = $input->getArguments();
257
258
        // When called via the Application, the first argument
259
        // will be the command name. The Application alters the
260
        // input definition to match, adding a 'command' argument
261
        // to the beginning.
262
        array_shift($args);
263
        if ($input instanceof PassThroughArgsInput) {
264
            return $this->appendPassThroughArgs($input, $args);
265
        }
266
        return $args;
267
    }
268
269
    protected function getArgsAndOptions($input)
270
    {
271
        if (!$input) {
272
            return [];
273
        }
274
        // Get passthrough args, and add the options on the end.
275
        $args = $this->getArgsWithPassThrough($input);
276
        $args[] = $input->getOptions();
277
        return $args;
278
    }
279
280
    protected function appendPassThroughArgs($input, $args)
281
    {
282
        $passThrough = $input->getPassThroughArgs();
283
        $definition = $this->getDefinition();
284
        $argumentDefinitions = $definition->getArguments();
285
        $lastParameter = end($argumentDefinitions);
286
        if ($lastParameter && $lastParameter->isArray()) {
287
            $args[$lastParameter->getName()] = array_merge($args[$lastParameter->getName()], $passThrough);
288
        } else {
289
            $args[$lastParameter->getName()] = implode(' ', $passThrough);
290
        }
291
        return $args;
292
    }
293
294
    /**
295
     * Returns all of the hook names that may be called for this command.
296
     *
297
     * @return array
298
     */
299
    protected function getNames()
300
    {
301
        return array_filter(
302
            array_merge(
303
                $this->getNamesUsingCommands(),
304
                [HookManager::getClassNameFromCallback($this->commandCallback)]
305
            )
306
        );
307
    }
308
309
    protected function getNamesUsingCommands()
310
    {
311
        return array_merge(
312
            [$this->getName()],
313
            $this->getAliases()
314
        );
315
    }
316
317
    /**
318
     * {@inheritdoc}
319
     */
320
    protected function interact(InputInterface $input, OutputInterface $output)
321
    {
322
        $this->commandProcessor()->interact(
323
            $input,
324
            $output,
325
            $this->getNames(),
326
            $this->annotationData
327
        );
328
    }
329
330
    /**
331
     * {@inheritdoc}
332
     */
333
    protected function execute(InputInterface $input, OutputInterface $output)
334
    {
335
        // Get passthrough args, and add the options on the end.
336
        $args = $this->getArgsAndOptions($input);
337
338
        if ($this->usesInputInterface) {
339
            array_unshift($args, $input);
340
        }
341
        if ($this->usesOutputInterface) {
342
            array_unshift($args, $output);
343
        }
344
345
        // Validate, run, process, alter, handle results.
346
        return $this->commandProcessor()->process(
347
            $output,
348
            $this->getNames(),
349
            $this->commandCallback,
350
            $this->annotationData,
351
            $args
352
        );
353
    }
354
355
    public function processResults(InputInterface $input, OutputInterface $output, $results)
356
    {
357
        $commandProcessor = $this->commandProcessor();
358
        $names = $this->getNames();
359
        $args = $this->getArgsAndOptions($input);
360
        $results = $commandProcessor->processResults(
361
            $names,
362
            $results,
363
            $args,
364
            $this->annotationData
365
        );
366
        $options = end($args);
367
        return $commandProcessor->handleResults(
368
            $output,
369
            $names,
370
            $results,
371
            $this->annotationData,
372
            $options
373
        );
374
    }
375
}
376