Completed
Push — master ( 8277a3...7a221a )
by Greg
10s
created

AnnotatedCommand::getArgsWithPassThrough()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
1
<?php
2
namespace Consolidation\AnnotatedCommand;
3
4
use Symfony\Component\Console\Command\Command;
5
use Symfony\Component\Console\Input\InputInterface;
6
use Symfony\Component\Console\Output\OutputInterface;
7
use Symfony\Component\Console\Input\InputOption;
8
use Symfony\Component\Console\Input\InputArgument;
9
use Consolidation\AnnotatedCommand\Hooks\HookManager;
10
use Consolidation\AnnotatedCommand\Parser\CommandInfo;
11
12
/**
13
 * AnnotatedCommands are created automatically by the
14
 * AnnotatedCommandFactory.  Each command method in a
15
 * command file will produce one AnnotatedCommand.  These
16
 * are then added to your Symfony Console Application object;
17
 * nothing else is needed.
18
 *
19
 * Optionally, though, you may extend AnnotatedCommand directly
20
 * to make a single command.  The usage pattern is the same
21
 * as for any other Symfony Console command, except that you may
22
 * omit the 'Confiure' method, and instead place your annotations
23
 * on the execute() method.
24
 *
25
 * @package Consolidation\AnnotatedCommand
26
 */
27
class AnnotatedCommand extends Command
28
{
29
    protected $commandCallback;
30
    protected $commandProcessor;
31
    protected $annotationData;
32
    protected $usesInputInterface;
33
    protected $usesOutputInterface;
34
    protected $returnType;
35
36
    public function __construct($name = null)
37
    {
38
        $commandInfo = false;
39
40
        // If this is a subclass of AnnotatedCommand, check to see
41
        // if the 'execute' method is annotated.  We could do this
42
        // unconditionally; it is a performance optimization to skip
43
        // checking the annotations if $this is an instance of
44
        // AnnotatedCommand.  Alternately, we break out a new subclass.
45
        // The command factory instantiates the subclass.
46
        if (get_class($this) != 'Consolidation\AnnotatedCommand\AnnotatedCommand') {
47
            $commandInfo = new CommandInfo($this, 'execute');
48
            if (!isset($name)) {
49
                $name = $commandInfo->getName();
50
            }
51
        }
52
        parent::__construct($name);
53
        if ($commandInfo && $commandInfo->hasAnnotation('command')) {
54
            $this->setCommandInfo($commandInfo);
55
        }
56
    }
57
58
    public function setCommandCallback($commandCallback)
59
    {
60
        $this->commandCallback = $commandCallback;
61
    }
62
63
    public function setCommandProcessor($commandProcessor)
64
    {
65
        $this->commandProcessor = $commandProcessor;
66
    }
67
68
    public function getCommandProcessor()
69
    {
70
        // If someone is using an AnnotatedCommand, and is NOT getting
71
        // it from an AnnotatedCommandFactory OR not correctly injecting
72
        // a command processor via setCommandProcessor() (ideally via the
73
        // DI container), then we'll just give each annotated command its
74
        // own command processor. This is not ideal; preferably, there would
75
        // only be one instance of the command processor in the application.
76
        if (!isset($this->commandProcessor)) {
77
            $this->commandProcessor = new CommandProcessor(new HookManager());
78
        }
79
        return $this->commandProcessor;
80
    }
81
82
    public function getReturnType()
83
    {
84
        return $this->returnType;
85
    }
86
87
    public function setReturnType($returnType)
88
    {
89
        $this->returnType = $returnType;
90
    }
91
92
    public function setAnnotationData($annotationData)
93
    {
94
        $this->annotationData = $annotationData;
95
    }
96
97
    public function setCommandInfo($commandInfo)
98
    {
99
        $this->setDescription($commandInfo->getDescription());
100
        $this->setHelp($commandInfo->getHelp());
101
        $this->setAliases($commandInfo->getAliases());
102
        $this->setAnnotationData($commandInfo->getAnnotations());
103
        foreach ($commandInfo->getExampleUsages() as $usage => $description) {
104
            // Symfony Console does not support attaching a description to a usage
105
            $this->addUsage($usage);
106
        }
107
        $this->setCommandArguments($commandInfo);
108
        $this->setCommandOptions($commandInfo);
109
        $this->setReturnType($commandInfo->getReturnType());
110
    }
111
112
    protected function setCommandArguments($commandInfo)
113
    {
114
        $this->setUsesInputInterface($commandInfo);
115
        $this->setUsesOutputInterface($commandInfo);
116
        $this->setCommandArgumentsFromParameters($commandInfo);
117
    }
118
119
    /**
120
     * Check whether the first parameter is an InputInterface.
121
     */
122
    protected function checkUsesInputInterface($params)
123
    {
124
        $firstParam = reset($params);
125
        return $firstParam instanceof InputInterface;
126
    }
127
128
    /**
129
     * Determine whether this command wants to get its inputs
130
     * via an InputInterface or via its command parameters
131
     */
132
    protected function setUsesInputInterface($commandInfo)
133
    {
134
        $params = $commandInfo->getParameters();
135
        $this->usesInputInterface = $this->checkUsesInputInterface($params);
136
    }
137
138
    /**
139
     * Determine whether this command wants to send its output directly
140
     * to the provided OutputInterface, or whether it will returned
141
     * structured output to be processed by the command processor.
142
     */
143
    protected function setUsesOutputInterface($commandInfo)
144
    {
145
        $params = $commandInfo->getParameters();
146
        $index = $this->checkUsesInputInterface($params) ? 1 : 0;
147
        $this->usesOutputInterface =
148
            (count($params) > $index) &&
149
            ($params[$index] instanceof OutputInterface);
150
    }
151
152
    protected function setCommandArgumentsFromParameters($commandInfo)
153
    {
154
        $args = $commandInfo->arguments()->getValues();
155
        foreach ($args as $name => $defaultValue) {
156
            $description = $commandInfo->arguments()->getDescription($name);
157
            $hasDefault = $commandInfo->arguments()->hasDefault($name);
158
            $parameterMode = $this->getCommandArgumentMode($hasDefault, $defaultValue);
159
            $this->addArgument($name, $parameterMode, $description, $defaultValue);
160
        }
161
    }
162
163
    protected function getCommandArgumentMode($hasDefault, $defaultValue)
164
    {
165
        if (!$hasDefault) {
166
            return InputArgument::REQUIRED;
167
        }
168
        if (is_array($defaultValue)) {
169
            return InputArgument::IS_ARRAY;
170
        }
171
        return InputArgument::OPTIONAL;
172
    }
173
174
    protected function setCommandOptions($commandInfo)
175
    {
176
        $opts = $commandInfo->options()->getValues();
177
        foreach ($opts as $name => $val) {
178
            $description = $commandInfo->options()->getDescription($name);
179
180
            $fullName = $name;
181
            $shortcut = '';
182
            if (strpos($name, '|')) {
183
                list($fullName, $shortcut) = explode('|', $name, 2);
184
            }
185
186
            if (is_bool($val)) {
187
                $this->addOption($fullName, $shortcut, InputOption::VALUE_NONE, $description);
188
            } else {
189
                $this->addOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $val);
190
            }
191
        }
192
    }
193
194
    protected function getArgsWithPassThrough($input)
195
    {
196
        $args = $input->getArguments();
197
198
        // When called via the Application, the first argument
199
        // will be the command name. The Application alters the
200
        // input definition to match, adding a 'command' argument
201
        // to the beginning.
202
        array_shift($args);
203
        if ($input instanceof PassThroughArgsInput) {
204
            return $this->appendPassThroughArgs($input, $args);
205
        }
206
        return $args;
207
    }
208
209
    protected function getArgsAndOptions($input)
210
    {
211
        if (!$input) {
212
            return [];
213
        }
214
        // Get passthrough args, and add the options on the end.
215
        $args = $this->getArgsWithPassThrough($input);
216
        $args[] = $input->getOptions();
217
        return $args;
218
    }
219
220
    protected function appendPassThroughArgs($input, $args)
221
    {
222
        $passThrough = $input->getPassThroughArgs();
223
        $definition = $this->getDefinition();
224
        $argumentDefinitions = $definition->getArguments();
225
        $lastParameter = end($argumentDefinitions);
226
        if ($lastParameter && $lastParameter->isArray()) {
227
            $args[$lastParameter->getName()] = array_merge($args[$lastParameter->getName()], $passThrough);
228
        } else {
229
            $args[$lastParameter->getName()] = implode(' ', $passThrough);
230
        }
231
        return $args;
232
    }
233
234
    /**
235
     * Returns all of the hook names that may be called for this command.
236
     *
237
     * @return array
238
     */
239
    protected function getNames()
240
    {
241
        return array_filter(
242
            array_merge(
243
                $this->getNamesUsingCommands(),
244
                [HookManager::getClassNameFromCallback($this->commandCallback)]
245
            )
246
        );
247
    }
248
249
    protected function getNamesUsingCommands()
250
    {
251
        return array_merge(
252
            [$this->getName()],
253
            $this->getAliases()
254
        );
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260
    protected function interact(InputInterface $input, OutputInterface $output)
261
    {
262
        $this->getCommandProcessor()->interact(
263
            $input,
264
            $output,
265
            $this->getNames(),
266
            $this->annotationData
267
        );
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     */
273
    protected function execute(InputInterface $input, OutputInterface $output)
274
    {
275
        // Get passthrough args, and add the options on the end.
276
        $args = $this->getArgsAndOptions($input);
277
278
        if ($this->usesInputInterface) {
279
            array_unshift($args, $input);
280
        }
281
        if ($this->usesOutputInterface) {
282
            array_unshift($args, $output);
283
        }
284
285
        // Validate, run, process, alter, handle results.
286
        return $this->getCommandProcessor()->process(
287
            $output,
288
            $this->getNames(),
289
            $this->commandCallback,
290
            $this->annotationData,
291
            $args
292
        );
293
    }
294
295
    public function processResults(InputInterface $input, OutputInterface $output, $results)
296
    {
297
        $commandProcessor = $this->getCommandProcessor();
298
        $names = $this->getNames();
299
        $args = $this->getArgsAndOptions($input);
300
        $results = $commandProcessor->processResults(
301
            $names,
302
            $results,
303
            $args,
304
            $this->annotationData
305
        );
306
        $options = end($args);
307
        return $commandProcessor->handleResults(
308
            $output,
309
            $names,
310
            $results,
311
            $this->annotationData,
312
            $options
313
        );
314
    }
315
}
316