Completed
Push — master ( 303d3b...22ea3e )
by Greg
07:12
created

AnnotatedCommand::execute()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 21
rs 9.3142
c 0
b 0
f 0
cc 3
eloc 12
nc 4
nop 2
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
    protected function getNames()
235
    {
236
        return array_merge(
237
            [$this->getName()],
238
            $this->getAliases()
239
        );
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245
    protected function interact(InputInterface $input, OutputInterface $output)
246
    {
247
        $this->getCommandProcessor()->interact(
248
            $input,
249
            $output,
250
            $this->getNames(),
251
            $this->annotationData
252
        );
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258
    protected function execute(InputInterface $input, OutputInterface $output)
259
    {
260
        // Get passthrough args, and add the options on the end.
261
        $args = $this->getArgsAndOptions($input);
262
263
        if ($this->usesInputInterface) {
264
            array_unshift($args, $input);
265
        }
266
        if ($this->usesOutputInterface) {
267
            array_unshift($args, $output);
268
        }
269
270
        // Validate, run, process, alter, handle results.
271
        return $this->getCommandProcessor()->process(
272
            $output,
273
            $this->getNames(),
274
            $this->commandCallback,
275
            $this->annotationData,
276
            $args
277
        );
278
    }
279
280
    public function processResults(InputInterface $input, OutputInterface $output, $results)
281
    {
282
        $commandProcessor = $this->getCommandProcessor();
283
        $names = $this->getNames();
284
        $args = $this->getArgsAndOptions($input);
285
        $results = $commandProcessor->processResults(
286
            $names,
287
            $results,
288
            $args,
289
            $this->annotationData
290
        );
291
        $options = end($args);
292
        return $commandProcessor->handleResults(
293
            $output,
294
            $names,
295
            $results,
296
            $this->annotationData,
297
            $options
298
        );
299
    }
300
}
301