Completed
Pull Request — master (#51)
by Greg
02:34
created

AnnotatedCommand   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 303
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 47
c 1
b 0
f 0
lcom 1
cbo 7
dl 0
loc 303
rs 8.439

26 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 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 10 1
A processResults() 0 17 1
A createCommandData() 0 10 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->getAnnotations());
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
    /**
229
     * Returns all of the hook names that may be called for this command.
230
     *
231
     * @return array
232
     */
233
    public function getNames()
234
    {
235
        return HookManager::getNames($this, $this->commandCallback);
236
    }
237
238
    /**
239
     * Add any options to this command that are defined by hook implementations
240
     */
241
    public function optionsHook()
242
    {
243
        $this->commandProcessor()->optionsHook(
244
            $this,
245
            $this->getNames(),
246
            $this->annotationData
247
        );
248
    }
249
250
    public function optionsHookForHookAnnotations($commandInfoList)
251
    {
252
        foreach ($commandInfoList as $commandInfo) {
253
            $inputOptions = $commandInfo->inputOptions();
254
            $this->addOptions($inputOptions);
255
            foreach ($commandInfo->getExampleUsages() as $usage => $description) {
256
                if (!in_array($usage, $this->getUsages())) {
257
                    $this->addUsage($usage);
258
                }
259
            }
260
        }
261
    }
262
263
    /**
264
     * {@inheritdoc}
265
     */
266
    protected function interact(InputInterface $input, OutputInterface $output)
267
    {
268
        $this->commandProcessor()->interact(
269
            $input,
270
            $output,
271
            $this->getNames(),
272
            $this->annotationData
273
        );
274
    }
275
276
    protected function initialize(InputInterface $input, OutputInterface $output)
277
    {
278
        // Allow the hook manager a chance to provide configuration values,
279
        // if there are any registered hooks to do that.
280
        $this->commandProcessor()->initializeHook($input, $this->getNames(), $this->annotationData);
281
    }
282
283
    /**
284
     * {@inheritdoc}
285
     */
286
    protected function execute(InputInterface $input, OutputInterface $output)
287
    {
288
        // Validate, run, process, alter, handle results.
289
        return $this->commandProcessor()->process(
290
            $output,
291
            $this->getNames(),
292
            $this->commandCallback,
293
            $this->createCommandData($input, $output)
294
        );
295
    }
296
297
    /**
298
     * This function is available for use by a class that may
299
     * wish to extend this class rather than use annotations to
300
     * define commands. Using this technique does allow for the
301
     * use of annotations to define hooks.
302
     */
303
    public function processResults(InputInterface $input, OutputInterface $output, $results)
304
    {
305
        $commandData = $this->createCommandData($input, $output);
306
        $commandProcessor = $this->commandProcessor();
307
        $names = $this->getNames();
308
        $results = $commandProcessor->processResults(
309
            $names,
310
            $results,
311
            $commandData
312
        );
313
        return $commandProcessor->handleResults(
314
            $output,
315
            $names,
316
            $results,
317
            $commandData
318
        );
319
    }
320
321
    protected function createCommandData(InputInterface $input, OutputInterface $output)
322
    {
323
        return new CommandData(
324
            $this->annotationData,
325
            $input,
326
            $output,
327
            $this->usesOutputInterface,
328
            $this->usesInputInterface
329
        );
330
    }
331
}
332