Completed
Push — master ( ef8ae5...2c2ae1 )
by Greg
02:18
created

AnnotatedCommand::addOptions()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 3
nop 2
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 getAnnotationData()
96
    {
97
        return $this->annotationData;
98
    }
99
100
    public function setAnnotationData($annotationData)
101
    {
102
        $this->annotationData = $annotationData;
103
    }
104
105
    public function setCommandInfo($commandInfo)
106
    {
107
        $this->setDescription($commandInfo->getDescription());
108
        $this->setHelp($commandInfo->getHelp());
109
        $this->setAliases($commandInfo->getAliases());
110
        $this->setAnnotationData($commandInfo->getAnnotationsForCommand());
111
        foreach ($commandInfo->getExampleUsages() as $usage => $description) {
112
            // Symfony Console does not support attaching a description to a usage
113
            $this->addUsage($usage);
114
        }
115
        $this->setCommandArguments($commandInfo);
116
        $this->setReturnType($commandInfo->getReturnType());
117
    }
118
119
    protected function setCommandArguments($commandInfo)
120
    {
121
        $this->setUsesInputInterface($commandInfo);
122
        $this->setUsesOutputInterface($commandInfo);
123
        $this->setCommandArgumentsFromParameters($commandInfo);
124
    }
125
126
    /**
127
     * Check whether the first parameter is an InputInterface.
128
     */
129
    protected function checkUsesInputInterface($params)
130
    {
131
        $firstParam = reset($params);
132
        return $firstParam instanceof InputInterface;
133
    }
134
135
    /**
136
     * Determine whether this command wants to get its inputs
137
     * via an InputInterface or via its command parameters
138
     */
139
    protected function setUsesInputInterface($commandInfo)
140
    {
141
        $params = $commandInfo->getParameters();
142
        $this->usesInputInterface = $this->checkUsesInputInterface($params);
143
    }
144
145
    /**
146
     * Determine whether this command wants to send its output directly
147
     * to the provided OutputInterface, or whether it will returned
148
     * structured output to be processed by the command processor.
149
     */
150
    protected function setUsesOutputInterface($commandInfo)
151
    {
152
        $params = $commandInfo->getParameters();
153
        $index = $this->checkUsesInputInterface($params) ? 1 : 0;
154
        $this->usesOutputInterface =
155
            (count($params) > $index) &&
156
            ($params[$index] instanceof OutputInterface);
157
    }
158
159
    protected function setCommandArgumentsFromParameters($commandInfo)
160
    {
161
        $args = $commandInfo->arguments()->getValues();
162
        foreach ($args as $name => $defaultValue) {
163
            $description = $commandInfo->arguments()->getDescription($name);
164
            $hasDefault = $commandInfo->arguments()->hasDefault($name);
165
            $parameterMode = $this->getCommandArgumentMode($hasDefault, $defaultValue);
166
            $this->addArgument($name, $parameterMode, $description, $defaultValue);
167
        }
168
    }
169
170
    protected function getCommandArgumentMode($hasDefault, $defaultValue)
171
    {
172
        if (!$hasDefault) {
173
            return InputArgument::REQUIRED;
174
        }
175
        if (is_array($defaultValue)) {
176
            return InputArgument::IS_ARRAY;
177
        }
178
        return InputArgument::OPTIONAL;
179
    }
180
181
    public function setCommandOptions($commandInfo, $automaticOptions = [])
182
    {
183
        $inputOptions = $commandInfo->inputOptions();
184
185
        $this->addOptions($inputOptions + $automaticOptions, $automaticOptions);
186
    }
187
188
    public function addOptions($inputOptions, $automaticOptions = [])
189
    {
190
        foreach ($inputOptions as $name => $inputOption) {
191
            $description = $inputOption->getDescription();
192
193
            if (empty($description) && isset($automaticOptions[$name])) {
194
                $description = $automaticOptions[$name]->getDescription();
195
                $inputOption = static::inputOptionSetDescription($inputOption, $description);
196
            }
197
            $this->getDefinition()->addOption($inputOption);
198
        }
199
    }
200
201
    protected static function inputOptionSetDescription($inputOption, $description)
202
    {
203
        // Recover the 'mode' value, because Symfony is stubborn
204
        $mode = 0;
205
        if ($inputOption->isValueRequired()) {
206
            $mode |= InputOption::VALUE_REQUIRED;
207
        }
208
        if ($inputOption->isValueOptional()) {
209
            $mode |= InputOption::VALUE_OPTIONAL;
210
        }
211
        if ($inputOption->isArray()) {
212
            $mode |= InputOption::VALUE_IS_ARRAY;
213
        }
214
        if (!$mode) {
215
            $mode = InputOption::VALUE_NONE;
216
        }
217
218
        $inputOption = new InputOption(
219
            $inputOption->getName(),
220
            $inputOption->getShortcut(),
221
            $mode,
222
            $description,
223
            $inputOption->getDefault()
224
        );
225
        return $inputOption;
226
    }
227
228
    protected function getArgsWithPassThrough($input)
229
    {
230
        $args = $input->getArguments();
231
232
        // When called via the Application, the first argument
233
        // will be the command name. The Application alters the
234
        // input definition to match, adding a 'command' argument
235
        // to the beginning.
236
        array_shift($args);
237
        if ($input instanceof PassThroughArgsInput) {
238
            return $this->appendPassThroughArgs($input, $args);
239
        }
240
        return $args;
241
    }
242
243
    protected function getArgsAndOptions($input)
244
    {
245
        if (!$input) {
246
            return [];
247
        }
248
        // Get passthrough args, and add the options on the end.
249
        $args = $this->getArgsWithPassThrough($input);
250
        $args['options'] = $input->getOptions();
251
        return $args;
252
    }
253
254
    protected function appendPassThroughArgs($input, $args)
255
    {
256
        $passThrough = $input->getPassThroughArgs();
257
        $definition = $this->getDefinition();
258
        $argumentDefinitions = $definition->getArguments();
259
        $lastParameter = end($argumentDefinitions);
260
        if ($lastParameter && $lastParameter->isArray()) {
261
            $args[$lastParameter->getName()] = array_merge($args[$lastParameter->getName()], $passThrough);
262
        } else {
263
            $args[$lastParameter->getName()] = implode(' ', $passThrough);
264
        }
265
        return $args;
266
    }
267
268
    /**
269
     * Returns all of the hook names that may be called for this command.
270
     *
271
     * @return array
272
     */
273
    protected function getNames()
274
    {
275
        return HookManager::getNames($this, $this->commandCallback);
276
    }
277
278
    /**
279
     * Add any options to this command that are defined by hook implementations
280
     */
281
    public function optionsHook()
282
    {
283
        $this->commandProcessor()->optionsHook(
284
            $this,
285
            $this->getNames(),
286
            $this->annotationData
287
        );
288
    }
289
290
    public function optionsHookForHookAnnotations($commandInfoList)
291
    {
292
        foreach ($commandInfoList as $commandInfo) {
293
            $inputOptions = $commandInfo->inputOptions();
294
            $this->addOptions($inputOptions);
295
            foreach ($commandInfo->getExampleUsages() as $usage => $description) {
296
                if (!in_array($usage, $this->getUsages())) {
297
                    $this->addUsage($usage);
298
                }
299
            }
300
        }
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     */
306
    protected function interact(InputInterface $input, OutputInterface $output)
307
    {
308
        $this->commandProcessor()->interact(
309
            $input,
310
            $output,
311
            $this->getNames(),
312
            $this->annotationData
313
        );
314
    }
315
316
    protected function initialize(InputInterface $input, OutputInterface $output)
317
    {
318
        // Allow the hook manager a chance to provide configuration values,
319
        // if there are any registered hooks to do that.
320
        $this->commandProcessor()->initializeHook($input, $this->getNames(), $this->annotationData);
321
    }
322
323
    /**
324
     * {@inheritdoc}
325
     */
326
    protected function execute(InputInterface $input, OutputInterface $output)
327
    {
328
        // Get passthrough args, and add the options on the end.
329
        $args = $this->getArgsAndOptions($input);
330
331
        if ($this->usesInputInterface) {
332
            array_unshift($args, $input);
333
        }
334
        if ($this->usesOutputInterface) {
335
            array_unshift($args, $output);
336
        }
337
338
        // Validate, run, process, alter, handle results.
339
        return $this->commandProcessor()->process(
340
            $output,
341
            $this->getNames(),
342
            $this->commandCallback,
343
            $this->annotationData,
344
            $args
345
        );
346
    }
347
348
    public function processResults(InputInterface $input, OutputInterface $output, $results)
349
    {
350
        $commandProcessor = $this->commandProcessor();
351
        $names = $this->getNames();
352
        $args = $this->getArgsAndOptions($input);
353
        $results = $commandProcessor->processResults(
354
            $names,
355
            $results,
356
            $args,
357
            $this->annotationData
358
        );
359
        $options = end($args);
360
        return $commandProcessor->handleResults(
361
            $output,
362
            $names,
363
            $results,
364
            $this->annotationData,
365
            $options
366
        );
367
    }
368
}
369