Completed
Pull Request — master (#42)
by Greg
02:53
created

AnnotatedCommand::getAnnotationData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
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 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
        $explicitOptions = $this->explicitOptions($commandInfo);
184
185
        $this->addOptions($explicitOptions + $automaticOptions, $automaticOptions);
186
    }
187
188
    protected function addOptions($inputOptions, $automaticOptions)
189
    {
190
        foreach ($inputOptions as $name => $inputOption) {
191
            $default = $inputOption->getDefault();
192
            $description = $inputOption->getDescription();
193
194
            if (empty($description) && isset($automaticOptions[$name])) {
195
                $description = $automaticOptions[$name]->getDescription();
196
            }
197
198
            // Recover the 'mode' value, because Symfony is stubborn
199
            $mode = 0;
200
            if ($inputOption->isValueRequired()) {
201
                $mode |= InputOption::VALUE_REQUIRED;
202
            }
203
            if ($inputOption->isValueOptional()) {
204
                $mode |= InputOption::VALUE_OPTIONAL;
205
            }
206
            if ($inputOption->isArray()) {
207
                $mode |= InputOption::VALUE_IS_ARRAY;
208
            }
209
            if (!$mode) {
210
                $mode = InputOption::VALUE_NONE;
211
                $default = null;
212
            }
213
214
            // Add the option; note that Symfony doesn't have a convenient
215
            // method to do this that takes an InputOption
216
            $this->addOption(
217
                $inputOption->getName(),
218
                $inputOption->getShortcut(),
219
                $mode,
220
                $description,
221
                $default
222
            );
223
        }
224
    }
225
226
    /**
227
     * Get the options that are explicitly defined, e.g. via
228
     * @option annotations, or via $options = ['someoption' => 'defaultvalue']
229
     * in the command method parameter list.
230
     *
231
     * @return InputOption[]
232
     */
233
    protected function explicitOptions($commandInfo)
234
    {
235
        $explicitOptions = [];
236
237
        $opts = $commandInfo->options()->getValues();
238
        foreach ($opts as $name => $defaultValue) {
239
            $description = $commandInfo->options()->getDescription($name);
240
241
            $fullName = $name;
242
            $shortcut = '';
243
            if (strpos($name, '|')) {
244
                list($fullName, $shortcut) = explode('|', $name, 2);
245
            }
246
247
            if (is_bool($defaultValue)) {
248
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_NONE, $description);
249
            } elseif ($defaultValue === InputOption::VALUE_REQUIRED) {
250
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_REQUIRED, $description);
251
            } else {
252
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $defaultValue);
253
            }
254
        }
255
256
        return $explicitOptions;
257
    }
258
259
    protected function getArgsWithPassThrough($input)
260
    {
261
        $args = $input->getArguments();
262
263
        // When called via the Application, the first argument
264
        // will be the command name. The Application alters the
265
        // input definition to match, adding a 'command' argument
266
        // to the beginning.
267
        array_shift($args);
268
        if ($input instanceof PassThroughArgsInput) {
269
            return $this->appendPassThroughArgs($input, $args);
270
        }
271
        return $args;
272
    }
273
274
    protected function getArgsAndOptions($input)
275
    {
276
        if (!$input) {
277
            return [];
278
        }
279
        // Get passthrough args, and add the options on the end.
280
        $args = $this->getArgsWithPassThrough($input);
281
        $args[] = $input->getOptions();
282
        return $args;
283
    }
284
285
    protected function appendPassThroughArgs($input, $args)
286
    {
287
        $passThrough = $input->getPassThroughArgs();
288
        $definition = $this->getDefinition();
289
        $argumentDefinitions = $definition->getArguments();
290
        $lastParameter = end($argumentDefinitions);
291
        if ($lastParameter && $lastParameter->isArray()) {
292
            $args[$lastParameter->getName()] = array_merge($args[$lastParameter->getName()], $passThrough);
293
        } else {
294
            $args[$lastParameter->getName()] = implode(' ', $passThrough);
295
        }
296
        return $args;
297
    }
298
299
    /**
300
     * Returns all of the hook names that may be called for this command.
301
     *
302
     * @return array
303
     */
304
    protected function getNames()
305
    {
306
        return array_filter(
307
            array_merge(
308
                $this->getNamesUsingCommands(),
309
                [HookManager::getClassNameFromCallback($this->commandCallback)]
310
            )
311
        );
312
    }
313
314
    protected function getNamesUsingCommands()
315
    {
316
        return array_merge(
317
            [$this->getName()],
318
            $this->getAliases()
319
        );
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     */
325
    protected function interact(InputInterface $input, OutputInterface $output)
326
    {
327
        $this->commandProcessor()->interact(
328
            $input,
329
            $output,
330
            $this->getNames(),
331
            $this->annotationData
332
        );
333
    }
334
335
    protected function initialize(InputInterface $input, OutputInterface $output)
336
    {
337
        // Allow the hook manager a chance to provide configuration values,
338
        // if there are any registered hooks to do that.
339
        $this->commandProcessor()->initializeHook($input, $this->getNames(), $this->annotationData);
340
    }
341
342
    /**
343
     * {@inheritdoc}
344
     */
345
    protected function execute(InputInterface $input, OutputInterface $output)
346
    {
347
        // Get passthrough args, and add the options on the end.
348
        $args = $this->getArgsAndOptions($input);
349
350
        if ($this->usesInputInterface) {
351
            array_unshift($args, $input);
352
        }
353
        if ($this->usesOutputInterface) {
354
            array_unshift($args, $output);
355
        }
356
357
        // Validate, run, process, alter, handle results.
358
        return $this->commandProcessor()->process(
359
            $output,
360
            $this->getNames(),
361
            $this->commandCallback,
362
            $this->annotationData,
363
            $args
364
        );
365
    }
366
367
    public function processResults(InputInterface $input, OutputInterface $output, $results)
368
    {
369
        $commandProcessor = $this->commandProcessor();
370
        $names = $this->getNames();
371
        $args = $this->getArgsAndOptions($input);
372
        $results = $commandProcessor->processResults(
373
            $names,
374
            $results,
375
            $args,
376
            $this->annotationData
377
        );
378
        $options = end($args);
379
        return $commandProcessor->handleResults(
380
            $output,
381
            $names,
382
            $results,
383
            $this->annotationData,
384
            $options
385
        );
386
    }
387
}
388