Completed
Pull Request — master (#44)
by Greg
02:15
created

AnnotatedCommand   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 323
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 6
dl 0
loc 323
rs 8.3206
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 22 5
A setCommandCallback() 0 4 1
A setCommandProcessor() 0 4 1
A commandProcessor() 0 13 2
A getReturnType() 0 4 1
A setReturnType() 0 4 1
A getAnnotationData() 0 4 1
A setAnnotationData() 0 4 1
A setCommandInfo() 0 13 2
A setCommandArguments() 0 6 1
A checkUsesInputInterface() 0 5 1
A setUsesInputInterface() 0 5 1
A setUsesOutputInterface() 0 8 3
A setCommandArgumentsFromParameters() 0 10 2
A getCommandArgumentMode() 0 10 3
A setCommandOptions() 0 6 1
A addOptions() 0 12 4
B inputOptionSetDescription() 0 26 5
A getArgsWithoutAppName() 0 11 1
A getArgsAndOptions() 0 10 2
A getNames() 0 4 1
A optionsHook() 0 8 1
A optionsHookForHookAnnotations() 0 12 4
A interact() 0 9 1
A initialize() 0 6 1
A execute() 0 21 3
A processResults() 0 20 1

How to fix   Complexity   

Complex Class

Complex classes like AnnotatedCommand often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AnnotatedCommand, and based on these observations, apply Extract Interface, too.

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 getArgsWithoutAppName($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
        return $args;
238
    }
239
240
    protected function getArgsAndOptions($input)
241
    {
242
        if (!$input) {
243
            return [];
244
        }
245
        // Get passthrough args, and add the options on the end.
246
        $args = $this->getArgsWithoutAppName($input);
247
        $args['options'] = $input->getOptions();
248
        return $args;
249
    }
250
251
    /**
252
     * Returns all of the hook names that may be called for this command.
253
     *
254
     * @return array
255
     */
256
    protected function getNames()
257
    {
258
        return HookManager::getNames($this, $this->commandCallback);
259
    }
260
261
    /**
262
     * Add any options to this command that are defined by hook implementations
263
     */
264
    public function optionsHook()
265
    {
266
        $this->commandProcessor()->optionsHook(
267
            $this,
268
            $this->getNames(),
269
            $this->annotationData
270
        );
271
    }
272
273
    public function optionsHookForHookAnnotations($commandInfoList)
274
    {
275
        foreach ($commandInfoList as $commandInfo) {
276
            $inputOptions = $commandInfo->inputOptions();
277
            $this->addOptions($inputOptions);
278
            foreach ($commandInfo->getExampleUsages() as $usage => $description) {
279
                if (!in_array($usage, $this->getUsages())) {
280
                    $this->addUsage($usage);
281
                }
282
            }
283
        }
284
    }
285
286
    /**
287
     * {@inheritdoc}
288
     */
289
    protected function interact(InputInterface $input, OutputInterface $output)
290
    {
291
        $this->commandProcessor()->interact(
292
            $input,
293
            $output,
294
            $this->getNames(),
295
            $this->annotationData
296
        );
297
    }
298
299
    protected function initialize(InputInterface $input, OutputInterface $output)
300
    {
301
        // Allow the hook manager a chance to provide configuration values,
302
        // if there are any registered hooks to do that.
303
        $this->commandProcessor()->initializeHook($input, $this->getNames(), $this->annotationData);
304
    }
305
306
    /**
307
     * {@inheritdoc}
308
     */
309
    protected function execute(InputInterface $input, OutputInterface $output)
310
    {
311
        // Get passthrough args, and add the options on the end.
312
        $args = $this->getArgsAndOptions($input);
313
314
        if ($this->usesInputInterface) {
315
            array_unshift($args, $input);
316
        }
317
        if ($this->usesOutputInterface) {
318
            array_unshift($args, $output);
319
        }
320
321
        // Validate, run, process, alter, handle results.
322
        return $this->commandProcessor()->process(
323
            $output,
324
            $this->getNames(),
325
            $this->commandCallback,
326
            $this->annotationData,
327
            $args
328
        );
329
    }
330
331
    public function processResults(InputInterface $input, OutputInterface $output, $results)
332
    {
333
        $commandProcessor = $this->commandProcessor();
334
        $names = $this->getNames();
335
        $args = $this->getArgsAndOptions($input);
336
        $results = $commandProcessor->processResults(
337
            $names,
338
            $results,
339
            $args,
340
            $this->annotationData
341
        );
342
        $options = end($args);
343
        return $commandProcessor->handleResults(
344
            $output,
345
            $names,
346
            $results,
347
            $this->annotationData,
348
            $options
349
        );
350
    }
351
}
352