Completed
Push — master ( 64c4bb...f05b3a )
by Greg
02:26
created

CommandInfo   C

Complexity

Total Complexity 65

Size/Duplication

Total Lines 392
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 65
c 3
b 0
f 0
lcom 1
cbo 1
dl 0
loc 392
rs 5.7894

36 Methods

Rating   Name   Duplication   Size   Complexity  
A getMethodName() 0 4 1
A getParameters() 0 4 1
A getDescription() 0 5 1
A setDescription() 0 4 1
A getHelp() 0 5 1
A setHelp() 0 4 1
A __construct() 0 10 1
A getAliases() 0 5 1
A setAliases() 0 7 2
A getExampleUsages() 0 5 1
A getName() 0 5 1
A setDefaultName() 0 4 1
A setName() 0 4 1
A determineAgumentClassifications() 0 15 4
A getArguments() 0 4 1
A hasArgument() 0 4 1
A setArgumentDefaultValue() 0 4 1
A addArgument() 0 10 4
B getArgumentClassification() 0 19 6
A getOptions() 0 4 1
A hasOption() 0 4 1
A setOptionDefaultValue() 0 4 1
A addOption() 0 10 4
A determineOptionsFromParameters() 0 15 4
A getArgumentDescription() 0 9 2
A getOptionDescription() 0 9 2
A isAssoc() 0 7 2
A getAnnotations() 0 5 1
A getAnnotation() 0 8 2
A hasAnnotation() 0 5 1
A convertName() 0 7 1
A setExampleUsage() 0 4 1
A parseDocBlock() 0 9 2
A addOtherAnnotation() 0 4 1
C findMatchingOption() 0 28 7
A convertListToCommaSeparated() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like CommandInfo 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 CommandInfo, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Consolidation\AnnotatedCommand;
3
4
/**
5
 * Given a class and method name, parse the annotations in the
6
 * DocBlock comment, and provide accessor methods for all of
7
 * the elements that are needed to create a Symfony Console Command.
8
 */
9
class CommandInfo
10
{
11
    /**
12
     * @var \ReflectionMethod
13
     */
14
    protected $reflection;
15
16
    /**
17
     * @var boolean
18
     * @var string
19
    */
20
    protected $docBlockIsParsed;
21
22
    /**
23
     * @var string
24
     */
25
    protected $name;
26
27
    /**
28
     * @var string
29
     */
30
    protected $description = '';
31
32
    /**
33
     * @var string
34
     */
35
    protected $help = '';
36
37
    /**
38
     * @var array
39
     */
40
    protected $options = [];
41
42
    /**
43
     * @var array
44
     */
45
    protected $arguments = [];
46
47
    /**
48
     * @var array
49
     */
50
    protected $argumentDescriptions = [];
51
52
    /**
53
     * @var array
54
     */
55
    protected $optionDescriptions = [];
56
57
    /**
58
     * @var array
59
     */
60
    protected $exampleUsage = [];
61
62
    /**
63
     * @var array
64
     */
65
    protected $otherAnnotations = [];
66
67
    /**
68
     * @var array
69
     */
70
    protected $aliases = [];
71
72
    /**
73
     * @var string
74
     */
75
    protected $methodName;
76
77
    public function __construct($classNameOrInstance, $methodName)
78
    {
79
        $this->reflection = new \ReflectionMethod($classNameOrInstance, $methodName);
80
        $this->methodName = $methodName;
81
        // Set up a default name for the command from the method name.
82
        // This can be overridden via @command or @name annotations.
83
        $this->setDefaultName();
84
        $this->options = $this->determineOptionsFromParameters();
85
        $this->arguments = $this->determineAgumentClassifications();
86
    }
87
88
    public function getMethodName()
89
    {
90
        return $this->methodName;
91
    }
92
93
    public function getParameters()
94
    {
95
        return $this->reflection->getParameters();
96
    }
97
98
    /**
99
     * Get the synopsis of the command (~first line).
100
     */
101
    public function getDescription()
102
    {
103
        $this->parseDocBlock();
104
        return $this->description;
105
    }
106
107
    public function setDescription($description)
108
    {
109
        $this->description = $description;
110
    }
111
112
    /**
113
     * Get the help text of the command (the description)
114
     */
115
    public function getHelp()
116
    {
117
        $this->parseDocBlock();
118
        return $this->help;
119
    }
120
121
    public function setHelp($help)
122
    {
123
        $this->help = $help;
124
    }
125
126
    public function getAliases()
127
    {
128
        $this->parseDocBlock();
129
        return $this->aliases;
130
    }
131
132
    public function setAliases($aliases)
133
    {
134
        if (is_string($aliases)) {
135
            $aliases = explode(',', static::convertListToCommaSeparated($aliases));
136
        }
137
        $this->aliases = array_filter($aliases);
138
    }
139
140
    public function getExampleUsages()
141
    {
142
        $this->parseDocBlock();
143
        return $this->exampleUsage;
144
    }
145
146
    public function getName()
147
    {
148
        $this->parseDocBlock();
149
        return $this->name;
150
    }
151
152
    public function setDefaultName()
153
    {
154
        $this->name = $this->convertName($this->reflection->name);
155
    }
156
157
    public function setName($name)
158
    {
159
        $this->name = $name;
160
    }
161
162
    protected function determineAgumentClassifications()
163
    {
164
        $args = [];
165
        $params = $this->reflection->getParameters();
166
        if (!empty($this->determineOptionsFromParameters())) {
167
            array_pop($params);
168
        }
169
        foreach ($params as $param) {
170
            $defaultValue = $this->getArgumentClassification($param);
171
            if ($defaultValue !== false) {
172
                $args[$param->name] = $defaultValue;
173
            }
174
        }
175
        return $args;
176
    }
177
178
    public function getArguments()
179
    {
180
        return $this->arguments;
181
    }
182
183
    public function hasArgument($name)
184
    {
185
        return array_key_exists($name, $this->arguments);
186
    }
187
188
    public function setArgumentDefaultValue($name, $defaultValue)
189
    {
190
        $this->arguments[$name] = $defaultValue;
191
    }
192
193
    public function addArgument($name, $description, $defaultValue = null)
194
    {
195
        if (!$this->hasArgument($name) || isset($defaultValue)) {
196
            $this->arguments[$name] = $defaultValue;
197
        }
198
        unset($this->argumentDescriptions[$name]);
199
        if (isset($description)) {
200
            $this->argumentDescriptions[$name] = $description;
201
        }
202
    }
203
204
    /**
205
     * Examine the provided parameter, and determine whether it
206
     * is a parameter that will be filled in with a positional
207
     * commandline argument.
208
     *
209
     * @return false|null|string|array
210
     */
211
    protected function getArgumentClassification($param)
212
    {
213
        $defaultValue = null;
214
        if ($param->isDefaultValueAvailable()) {
215
            $defaultValue = $param->getDefaultValue();
216
            if ($this->isAssoc($defaultValue)) {
217
                return false;
218
            }
219
        }
220
        if ($param->isArray()) {
221
            return [];
222
        }
223
        // Commandline arguments must be strings, so ignore
224
        // any parameter that is typehinted to anything else.
225
        if (($param->getClass() != null) && ($param->getClass() != 'string')) {
226
            return false;
227
        }
228
        return $defaultValue;
229
    }
230
231
    public function getOptions()
232
    {
233
        return $this->options;
234
    }
235
236
    public function hasOption($name)
237
    {
238
        return array_key_exists($name, $this->options);
239
    }
240
241
    public function setOptionDefaultValue($name, $defaultValue)
242
    {
243
        $this->options[$name] = $defaultValue;
244
    }
245
246
    public function addOption($name, $description, $defaultValue = false)
247
    {
248
        if (!$this->hasOption($name) || $defaultValue) {
249
            $this->options[$name] = $defaultValue;
250
        }
251
        unset($this->optionDescriptions[$name]);
252
        if (isset($description)) {
253
            $this->optionDescriptions[$name] = $description;
254
        }
255
    }
256
257
    public function determineOptionsFromParameters()
258
    {
259
        $params = $this->reflection->getParameters();
260
        if (empty($params)) {
261
            return [];
262
        }
263
        $param = end($params);
264
        if (!$param->isDefaultValueAvailable()) {
265
            return [];
266
        }
267
        if (!$this->isAssoc($param->getDefaultValue())) {
268
            return [];
269
        }
270
        return $param->getDefaultValue();
271
    }
272
273
    public function getArgumentDescription($name)
274
    {
275
        $this->parseDocBlock();
276
        if (array_key_exists($name, $this->argumentDescriptions)) {
277
            return $this->argumentDescriptions[$name];
278
        }
279
280
        return '';
281
    }
282
283
    public function getOptionDescription($name)
284
    {
285
        $this->parseDocBlock();
286
        if (array_key_exists($name, $this->optionDescriptions)) {
287
            return $this->optionDescriptions[$name];
288
        }
289
290
        return '';
291
    }
292
293
    protected function isAssoc($arr)
294
    {
295
        if (!is_array($arr)) {
296
            return false;
297
        }
298
        return array_keys($arr) !== range(0, count($arr) - 1);
299
    }
300
301
    public function getAnnotations()
302
    {
303
        $this->parseDocBlock();
304
        return $this->otherAnnotations;
305
    }
306
307
    public function getAnnotation($annotation)
308
    {
309
        // hasAnnotation parses the docblock
310
        if (!$this->hasAnnotation($annotation)) {
311
            return null;
312
        }
313
        return $this->otherAnnotations[$annotation];
314
    }
315
316
    public function hasAnnotation($annotation)
317
    {
318
        $this->parseDocBlock();
319
        return array_key_exists($annotation, $this->otherAnnotations);
320
    }
321
322
    protected function convertName($camel)
323
    {
324
        $splitter="-";
325
        $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel));
326
        $camel = preg_replace("/$splitter/", ':', $camel, 1);
327
        return strtolower($camel);
328
    }
329
330
    public function setExampleUsage($usage, $description)
331
    {
332
        $this->exampleUsage[$usage] = $description;
333
    }
334
335
    /**
336
     * Parse the docBlock comment for this command, and set the
337
     * fields of this class with the data thereby obtained.
338
     */
339
    protected function parseDocBlock()
340
    {
341
        if (!$this->docBlockIsParsed) {
342
            $docblock = $this->reflection->getDocComment();
343
            $parser = new CommandDocBlockParser($this);
344
            $parser->parse($docblock);
345
            $this->docBlockIsParsed = true;
346
        }
347
    }
348
349
    /**
350
     * Save any tag that we do not explicitly recognize in the
351
     * 'otherAnnotations' map.
352
     */
353
    public function addOtherAnnotation($name, $content)
354
    {
355
        $this->otherAnnotations[$name] = $content;
356
    }
357
358
    /**
359
     * An option might have a name such as 'silent|s'. In this
360
     * instance, we will allow the @option or @default tag to
361
     * reference the option only by name (e.g. 'silent' or 's'
362
     * instead of 'silent|s').
363
     */
364
    public function findMatchingOption($optionName)
365
    {
366
        // Exit fast if there's an exact match
367
        if (isset($this->options[$optionName])) {
368
            return $optionName;
369
        }
370
        // Check to see if we can find the option name in an existing option,
371
        // e.g. if the options array has 'silent|s' => false, and the annotation
372
        // is @silent.
373
        foreach ($this->options as $name => $default) {
374
            if (in_array($optionName, explode('|', $name))) {
375
                return $name;
376
            }
377
        }
378
        // Check the other direction: if the annotation contains @silent|s
379
        // and the options array has 'silent|s'.
380
        $checkMatching = explode('|', $optionName);
381
        if (count($checkMatching) > 1) {
382
            foreach ($checkMatching as $checkName) {
383
                if (isset($this->options[$checkName])) {
384
                    $this->options[$optionName] = $this->options[$checkName];
385
                    unset($this->options[$checkName]);
386
                    return $optionName;
387
                }
388
            }
389
        }
390
        return $optionName;
391
    }
392
    /**
393
     * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
394
     * convert the data into the last of these forms.
395
     */
396
    protected static function convertListToCommaSeparated($text)
397
    {
398
        return preg_replace('#[ \t\n\r,]+#', ',', $text);
399
    }
400
}
401