Completed
Pull Request — master (#81)
by Greg
02:12
created

CommandInfo   D

Complexity

Total Complexity 98

Size/Duplication

Total Lines 771
Duplicated Lines 6.23 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 98
c 0
b 0
f 0
lcom 1
cbo 4
dl 48
loc 771
rs 4.4444

45 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 14 4
A create() 0 4 1
A deserialize() 9 9 1
A cachedMethodExists() 9 9 1
B isValidSerializedData() 0 10 6
A cachedFileIsModified() 0 5 1
A constructFromClassAndMethod() 0 9 1
B constructFromCache() 14 42 6
F serialize() 16 66 12
A defaultSerializationData() 0 17 1
A getMethodName() 0 4 1
A getName() 0 5 1
A setName() 0 5 1
A getReturnType() 0 5 1
A setReturnType() 0 5 1
A getRawAnnotations() 0 5 1
A getAnnotations() 0 13 1
A getAnnotation() 0 8 2
A hasAnnotation() 0 5 1
A addAnnotation() 0 4 1
A removeAnnotation() 0 4 1
A getDescription() 0 5 1
A setDescription() 0 5 1
A getHelp() 0 5 1
A setHelp() 0 5 1
A getAliases() 0 5 1
A setAliases() 0 8 2
A getExampleUsages() 0 5 1
A setExampleUsage() 0 5 1
A getTopics() 0 8 2
A getParameters() 0 4 1
A arguments() 0 4 1
A options() 0 4 1
A inputOptions() 0 7 2
C createInputOptions() 0 34 8
A findMatchingOption() 0 12 3
A findOptionAmongAlternatives() 0 15 4
A findExistingOption() 0 11 3
A determineAgumentClassifications() 0 13 3
B addParameterToResult() 0 17 5
A determineOptionsFromParameters() 0 15 4
A isAssoc() 0 7 2
A convertName() 0 7 1
A parseDocBlock() 0 9 2
A convertListToCommaSeparated() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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\Parser;
3
4
use Symfony\Component\Console\Input\InputOption;
5
use Consolidation\AnnotatedCommand\Parser\Internal\CommandDocBlockParser;
6
use Consolidation\AnnotatedCommand\Parser\Internal\CommandDocBlockParserFactory;
7
use Consolidation\AnnotatedCommand\AnnotationData;
8
9
/**
10
 * Given a class and method name, parse the annotations in the
11
 * DocBlock comment, and provide accessor methods for all of
12
 * the elements that are needed to create a Symfony Console Command.
13
 *
14
 * Note that the name of this class is now somewhat of a misnomer,
15
 * as we now use it to hold annotation data for hooks as well as commands.
16
 * It would probably be better to rename this to MethodInfo at some point.
17
 */
18
class CommandInfo
19
{
20
    /**
21
     * Serialization schema version. Incremented every time the serialization schema changes.
22
     */
23
    const SERIALIZATION_SCHEMA_VERSION = 1;
24
25
    /**
26
     * @var \ReflectionMethod
27
     */
28
    protected $reflection;
29
30
    /**
31
     * @var boolean
32
     * @var string
33
    */
34
    protected $docBlockIsParsed = false;
35
36
    /**
37
     * @var string
38
     */
39
    protected $name;
40
41
    /**
42
     * @var string
43
     */
44
    protected $description = '';
45
46
    /**
47
     * @var string
48
     */
49
    protected $help = '';
50
51
    /**
52
     * @var DefaultsWithDescriptions
53
     */
54
    protected $options;
55
56
    /**
57
     * @var DefaultsWithDescriptions
58
     */
59
    protected $arguments;
60
61
    /**
62
     * @var array
63
     */
64
    protected $exampleUsage = [];
65
66
    /**
67
     * @var AnnotationData
68
     */
69
    protected $otherAnnotations;
70
71
    /**
72
     * @var array
73
     */
74
    protected $aliases = [];
75
76
    /**
77
     * @var InputOption[]
78
     */
79
    protected $inputOptions;
80
81
    /**
82
     * @var string
83
     */
84
    protected $methodName;
85
86
    /**
87
     * @var string
88
     */
89
    protected $returnType;
90
91
    /**
92
     * Create a new CommandInfo class for a particular method of a class.
93
     *
94
     * @param string|mixed $classNameOrInstance The name of a class, or an
95
     *   instance of it, or an array of cached data.
96
     * @param string $methodName The name of the method to get info about.
97
     * @param array $cache Cached data
98
     * @deprecated Use CommandInfo::create() or CommandInfo::deserialize()
99
     *   instead. In the future, this constructor will be protected.
100
     */
101
    public function __construct($classNameOrInstance, $methodName, $cache = [])
102
    {
103
        $this->reflection = new \ReflectionMethod($classNameOrInstance, $methodName);
104
        $this->methodName = $methodName;
105
106
        // If the cache came from a newer version, ignore it and
107
        // regenerate the cached information.
108
        if (!empty($cache) && static::isValidSerializedData($cache) && !$this->cachedFileIsModified($cache)) {
109
            $this->constructFromCache($cache);
110
            $this->docBlockIsParsed = true;
111
        } else {
112
            $this->constructFromClassAndMethod($classNameOrInstance, $methodName);
113
        }
114
    }
115
116
    public static function create($classNameOrInstance, $methodName)
117
    {
118
        return new self($classNameOrInstance, $methodName);
119
    }
120
121 View Code Duplication
    public static function deserialize($cache)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
122
    {
123
        $cache = (array)$cache;
124
125
        $className = $cache['class'];
126
        $methodName = $cache['method_name'];
127
128
        return new self($className, $methodName, $cache);
129
    }
130
131 View Code Duplication
    protected static function cachedMethodExists($cache)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
132
    {
133
        $cache = (array)$cache;
134
135
        $className = $cache['class'];
136
        $methodName = $cache['method_name'];
137
138
        return method_exists($className, $methodName);
139
    }
140
141
    public static function isValidSerializedData($cache)
142
    {
143
        return
144
            isset($cache['schema']) &&
145
            isset($cache['method_name']) &&
146
            isset($cache['mtime']) &&
147
            ($cache['schema'] > 0) &&
148
            ($cache['schema'] <= self::SERIALIZATION_SCHEMA_VERSION) &&
149
            self::cachedMethodExists($cache);
150
    }
151
152
    public function cachedFileIsModified($cache)
153
    {
154
        $path = $this->reflection->getFileName();
155
        return filemtime($path) != $cache['mtime'];
156
    }
157
158
    protected function constructFromClassAndMethod($classNameOrInstance, $methodName)
0 ignored issues
show
Unused Code introduced by
The parameter $classNameOrInstance is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
159
    {
160
        $this->otherAnnotations = new AnnotationData();
161
        // Set up a default name for the command from the method name.
162
        // This can be overridden via @command or @name annotations.
163
        $this->name = $this->convertName($methodName);
164
        $this->options = new DefaultsWithDescriptions($this->determineOptionsFromParameters(), false);
165
        $this->arguments = $this->determineAgumentClassifications();
166
    }
167
168
    protected function constructFromCache($info_array)
169
    {
170
        $info_array += $this->defaultSerializationData();
171
172
        $this->name = $info_array['name'];
173
        $this->methodName = $info_array['method_name'];
174
        $this->otherAnnotations = new AnnotationData((array) $info_array['annotations']);
175
        $this->arguments = new DefaultsWithDescriptions();
176
        $this->options = new DefaultsWithDescriptions();
177
        $this->aliases = $info_array['aliases'];
178
        $this->help = $info_array['help'];
179
        $this->description = $info_array['description'];
180
        $this->exampleUsage = $info_array['example_usages'];
181
        $this->returnType = $info_array['return_type'];
0 ignored issues
show
Documentation Bug introduced by
It seems like $info_array['return_type'] of type array is incompatible with the declared type string of property $returnType.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
182
183 View Code Duplication
        foreach ((array)$info_array['arguments'] as $key => $info) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
184
            $info = (array)$info;
185
            $this->arguments->add($key, $info['description']);
186
            if (array_key_exists('default', $info)) {
187
                $this->arguments->setDefaultValue($key, $info['default']);
188
            }
189
        }
190 View Code Duplication
        foreach ((array)$info_array['options'] as $key => $info) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
191
            $info = (array)$info;
192
            $this->options->add($key, $info['description']);
193
            if (array_key_exists('default', $info)) {
194
                $this->options->setDefaultValue($key, $info['default']);
195
            }
196
        }
197
198
        $this->input_options = [];
0 ignored issues
show
Bug introduced by
The property input_options does not seem to exist. Did you mean options?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
199
        foreach ((array)$info_array['input_options'] as $i => $option) {
200
            $option = (array) $option;
201
            $this->inputOptions[$i] = new InputOption(
202
                $option['name'],
203
                $option['shortcut'],
204
                $option['mode'],
205
                $option['description'],
206
                $option['default']
207
            );
208
        }
209
    }
210
211
    public function serialize()
212
    {
213
        $path = $this->reflection->getFileName();
214
215
        $info = [
216
            'schema' => self::SERIALIZATION_SCHEMA_VERSION,
217
            'class' => $this->reflection->getDeclaringClass()->getName(),
218
            'method_name' => $this->getMethodName(),
219
            'name' => $this->getName(),
220
            'description' => $this->getDescription(),
221
            'help' => $this->getHelp(),
222
            'aliases' => $this->getAliases(),
223
            'annotations' => $this->getAnnotations()->getArrayCopy(),
224
            // Todo: Test This.
225
            'topics' => $this->getTopics(),
226
            'example_usages' => $this->getExampleUsages(),
227
            'return_type' => $this->getReturnType(),
228
            'mtime' => filemtime($path),
229
        ] + $this->defaultSerializationData();
230 View Code Duplication
        foreach ($this->arguments()->getValues() as $key => $val) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
231
            $info['arguments'][$key] = [
232
                'description' => $this->arguments()->getDescription($key),
233
            ];
234
            if ($this->arguments()->hasDefault($key)) {
235
                $info['arguments'][$key]['default'] = $val;
236
            }
237
        }
238 View Code Duplication
        foreach ($this->options()->getValues() as $key => $val) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
239
            $info['options'][$key] = [
240
                'description' => $this->options()->getDescription($key),
241
            ];
242
            if ($this->options()->hasDefault($key)) {
243
                $info['options'][$key]['default'] = $val;
244
            }
245
        }
246
        foreach ($this->getParameters() as $i => $parameter) {
0 ignored issues
show
Unused Code introduced by
This foreach statement is empty and can be removed.

This check looks for foreach loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
247
            // TODO: Also cache input/output params
248
        }
249
        foreach ($this->inputOptions() as $i => $option) {
250
            $mode = 0;
251
            if ($option->isValueRequired()) {
252
                $mode |= InputOption::VALUE_REQUIRED;
253
            }
254
            if ($option->isValueOptional()) {
255
                $mode |= InputOption::VALUE_OPTIONAL;
256
            }
257
            if ($option->isArray()) {
258
                $mode |= InputOption::VALUE_IS_ARRAY;
259
            }
260
            if (!$mode) {
261
                $mode = InputOption::VALUE_NONE;
262
            }
263
264
            $info['input_options'][$i] = [
265
                'name' => $option->getName(),
266
                'shortcut' => $option->getShortcut(),
267
                'mode' => $mode,
268
                'description' => $option->getDescription(),
269
                'default' => null,
270
            ];
271
            if ($option->isValueOptional()) {
272
                $info['input_options'][$i]['default'] = $option->getDefault();
273
            }
274
        }
275
        return $info;
276
    }
277
278
    /**
279
     * Default data for serialization.
280
     * @return array
281
     */
282
    protected function defaultSerializationData()
283
    {
284
        return [
285
            'description' => '',
286
            'help' => '',
287
            'aliases' => [],
288
            'annotations' => [],
289
            'topics' => [],
290
            'example_usages' => [],
291
            'return_type' => [],
292
            'parameters' => [],
293
            'arguments' => [],
294
            'options' => [],
295
            'input_options' => [],
296
            'mtime' => 0,
297
        ];
298
    }
299
300
    /**
301
     * Recover the method name provided to the constructor.
302
     *
303
     * @return string
304
     */
305
    public function getMethodName()
306
    {
307
        return $this->methodName;
308
    }
309
310
    /**
311
     * Return the primary name for this command.
312
     *
313
     * @return string
314
     */
315
    public function getName()
316
    {
317
        $this->parseDocBlock();
318
        return $this->name;
319
    }
320
321
    /**
322
     * Set the primary name for this command.
323
     *
324
     * @param string $name
325
     */
326
    public function setName($name)
327
    {
328
        $this->name = $name;
329
        return $this;
330
    }
331
332
    public function getReturnType()
333
    {
334
        $this->parseDocBlock();
335
        return $this->returnType;
336
    }
337
338
    public function setReturnType($returnType)
339
    {
340
        $this->returnType = $returnType;
341
        return $this;
342
    }
343
344
    /**
345
     * Get any annotations included in the docblock comment for the
346
     * implementation method of this command that are not already
347
     * handled by the primary methods of this class.
348
     *
349
     * @return AnnotationData
350
     */
351
    public function getRawAnnotations()
352
    {
353
        $this->parseDocBlock();
354
        return $this->otherAnnotations;
355
    }
356
357
    /**
358
     * Get any annotations included in the docblock comment,
359
     * also including default values such as @command.  We add
360
     * in the default @command annotation late, and only in a
361
     * copy of the annotation data because we use the existance
362
     * of a @command to indicate that this CommandInfo is
363
     * a command, and not a hook or anything else.
364
     *
365
     * @return AnnotationData
366
     */
367
    public function getAnnotations()
368
    {
369
        // Also provide the path to the commandfile that
370
        // these annotations were pulled from.
371
        $path = $this->reflection->getFileName();
372
        return new AnnotationData(
373
            $this->getRawAnnotations()->getArrayCopy() +
374
            [
375
                'command' => $this->getName(),
376
                '_path' => $path,
377
            ]
378
        );
379
    }
380
381
    /**
382
     * Return a specific named annotation for this command.
383
     *
384
     * @param string $annotation The name of the annotation.
385
     * @return string
386
     */
387
    public function getAnnotation($annotation)
388
    {
389
        // hasAnnotation parses the docblock
390
        if (!$this->hasAnnotation($annotation)) {
391
            return null;
392
        }
393
        return $this->otherAnnotations[$annotation];
394
    }
395
396
    /**
397
     * Check to see if the specified annotation exists for this command.
398
     *
399
     * @param string $annotation The name of the annotation.
400
     * @return boolean
401
     */
402
    public function hasAnnotation($annotation)
403
    {
404
        $this->parseDocBlock();
405
        return isset($this->otherAnnotations[$annotation]);
406
    }
407
408
    /**
409
     * Save any tag that we do not explicitly recognize in the
410
     * 'otherAnnotations' map.
411
     */
412
    public function addAnnotation($name, $content)
413
    {
414
        $this->otherAnnotations[$name] = $content;
415
    }
416
417
    /**
418
     * Remove an annotation that was previoudly set.
419
     */
420
    public function removeAnnotation($name)
421
    {
422
        unset($this->otherAnnotations[$name]);
423
    }
424
425
    /**
426
     * Get the synopsis of the command (~first line).
427
     *
428
     * @return string
429
     */
430
    public function getDescription()
431
    {
432
        $this->parseDocBlock();
433
        return $this->description;
434
    }
435
436
    /**
437
     * Set the command description.
438
     *
439
     * @param string $description The description to set.
440
     */
441
    public function setDescription($description)
442
    {
443
        $this->description = $description;
444
        return $this;
445
    }
446
447
    /**
448
     * Get the help text of the command (the description)
449
     */
450
    public function getHelp()
451
    {
452
        $this->parseDocBlock();
453
        return $this->help;
454
    }
455
    /**
456
     * Set the help text for this command.
457
     *
458
     * @param string $help The help text.
459
     */
460
    public function setHelp($help)
461
    {
462
        $this->help = $help;
463
        return $this;
464
    }
465
466
    /**
467
     * Return the list of aliases for this command.
468
     * @return string[]
469
     */
470
    public function getAliases()
471
    {
472
        $this->parseDocBlock();
473
        return $this->aliases;
474
    }
475
476
    /**
477
     * Set aliases that can be used in place of the command's primary name.
478
     *
479
     * @param string|string[] $aliases
480
     */
481
    public function setAliases($aliases)
482
    {
483
        if (is_string($aliases)) {
484
            $aliases = explode(',', static::convertListToCommaSeparated($aliases));
485
        }
486
        $this->aliases = array_filter($aliases);
487
        return $this;
488
    }
489
490
    /**
491
     * Return the examples for this command. This is @usage instead of
492
     * @example because the later is defined by the phpdoc standard to
493
     * be example method calls.
494
     *
495
     * @return string[]
496
     */
497
    public function getExampleUsages()
498
    {
499
        $this->parseDocBlock();
500
        return $this->exampleUsage;
501
    }
502
503
    /**
504
     * Add an example usage for this command.
505
     *
506
     * @param string $usage An example of the command, including the command
507
     *   name and all of its example arguments and options.
508
     * @param string $description An explanation of what the example does.
509
     */
510
    public function setExampleUsage($usage, $description)
511
    {
512
        $this->exampleUsage[$usage] = $description;
513
        return $this;
514
    }
515
516
    /**
517
     * Return the topics for this command.
518
     *
519
     * @return string[]
520
     */
521
    public function getTopics()
522
    {
523
        if (!$this->hasAnnotation('topics')) {
524
            return [];
525
        }
526
        $topics = $this->getAnnotation('topics');
527
        return explode(',', trim($topics));
528
    }
529
530
    /**
531
     * Return the list of refleaction parameters.
532
     *
533
     * @return ReflectionParameter[]
534
     */
535
    public function getParameters()
536
    {
537
        return $this->reflection->getParameters();
538
    }
539
540
    /**
541
     * Descriptions of commandline arguements for this command.
542
     *
543
     * @return DefaultsWithDescriptions
544
     */
545
    public function arguments()
546
    {
547
        return $this->arguments;
548
    }
549
550
    /**
551
     * Descriptions of commandline options for this command.
552
     *
553
     * @return DefaultsWithDescriptions
554
     */
555
    public function options()
556
    {
557
        return $this->options;
558
    }
559
560
    /**
561
     * Get the inputOptions for the options associated with this CommandInfo
562
     * object, e.g. via @option annotations, or from
563
     * $options = ['someoption' => 'defaultvalue'] in the command method
564
     * parameter list.
565
     *
566
     * @return InputOption[]
567
     */
568
    public function inputOptions()
569
    {
570
        if (!isset($this->inputOptions)) {
571
            $this->inputOptions = $this->createInputOptions();
572
        }
573
        return $this->inputOptions;
574
    }
575
576
    protected function createInputOptions()
577
    {
578
        $explicitOptions = [];
579
580
        $opts = $this->options()->getValues();
581
        foreach ($opts as $name => $defaultValue) {
582
            $description = $this->options()->getDescription($name);
583
584
            $fullName = $name;
585
            $shortcut = '';
586
            if (strpos($name, '|')) {
587
                list($fullName, $shortcut) = explode('|', $name, 2);
588
            }
589
590
            if (is_bool($defaultValue)) {
591
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_NONE, $description);
592
            } elseif ($defaultValue === InputOption::VALUE_REQUIRED) {
593
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_REQUIRED, $description);
594
            } elseif (is_array($defaultValue)) {
595
                $optionality = count($defaultValue) ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_REQUIRED;
596
                $explicitOptions[$fullName] = new InputOption(
597
                    $fullName,
598
                    $shortcut,
599
                    InputOption::VALUE_IS_ARRAY | $optionality,
600
                    $description,
601
                    count($defaultValue) ? $defaultValue : null
602
                );
603
            } else {
604
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $defaultValue);
605
            }
606
        }
607
608
        return $explicitOptions;
609
    }
610
611
    /**
612
     * An option might have a name such as 'silent|s'. In this
613
     * instance, we will allow the @option or @default tag to
614
     * reference the option only by name (e.g. 'silent' or 's'
615
     * instead of 'silent|s').
616
     *
617
     * @param string $optionName
618
     * @return string
619
     */
620
    public function findMatchingOption($optionName)
621
    {
622
        // Exit fast if there's an exact match
623
        if ($this->options->exists($optionName)) {
624
            return $optionName;
625
        }
626
        $existingOptionName = $this->findExistingOption($optionName);
627
        if (isset($existingOptionName)) {
628
            return $existingOptionName;
629
        }
630
        return $this->findOptionAmongAlternatives($optionName);
631
    }
632
633
    /**
634
     * @param string $optionName
635
     * @return string
636
     */
637
    protected function findOptionAmongAlternatives($optionName)
638
    {
639
        // Check the other direction: if the annotation contains @silent|s
640
        // and the options array has 'silent|s'.
641
        $checkMatching = explode('|', $optionName);
642
        if (count($checkMatching) > 1) {
643
            foreach ($checkMatching as $checkName) {
644
                if ($this->options->exists($checkName)) {
645
                    $this->options->rename($checkName, $optionName);
646
                    return $optionName;
647
                }
648
            }
649
        }
650
        return $optionName;
651
    }
652
653
    /**
654
     * @param string $optionName
655
     * @return string|null
656
     */
657
    protected function findExistingOption($optionName)
658
    {
659
        // Check to see if we can find the option name in an existing option,
660
        // e.g. if the options array has 'silent|s' => false, and the annotation
661
        // is @silent.
662
        foreach ($this->options()->getValues() as $name => $default) {
663
            if (in_array($optionName, explode('|', $name))) {
664
                return $name;
665
            }
666
        }
667
    }
668
669
    /**
670
     * Examine the parameters of the method for this command, and
671
     * build a list of commandline arguements for them.
672
     *
673
     * @return array
674
     */
675
    protected function determineAgumentClassifications()
676
    {
677
        $result = new DefaultsWithDescriptions();
678
        $params = $this->reflection->getParameters();
679
        $optionsFromParameters = $this->determineOptionsFromParameters();
680
        if (!empty($optionsFromParameters)) {
681
            array_pop($params);
682
        }
683
        foreach ($params as $param) {
684
            $this->addParameterToResult($result, $param);
685
        }
686
        return $result;
687
    }
688
689
    /**
690
     * Examine the provided parameter, and determine whether it
691
     * is a parameter that will be filled in with a positional
692
     * commandline argument.
693
     */
694
    protected function addParameterToResult($result, $param)
695
    {
696
        // Commandline arguments must be strings, so ignore any
697
        // parameter that is typehinted to any non-primative class.
698
        if ($param->getClass() != null) {
699
            return;
700
        }
701
        $result->add($param->name);
702
        if ($param->isDefaultValueAvailable()) {
703
            $defaultValue = $param->getDefaultValue();
704
            if (!$this->isAssoc($defaultValue)) {
705
                $result->setDefaultValue($param->name, $defaultValue);
706
            }
707
        } elseif ($param->isArray()) {
708
            $result->setDefaultValue($param->name, []);
709
        }
710
    }
711
712
    /**
713
     * Examine the parameters of the method for this command, and determine
714
     * the disposition of the options from them.
715
     *
716
     * @return array
717
     */
718
    protected function determineOptionsFromParameters()
719
    {
720
        $params = $this->reflection->getParameters();
721
        if (empty($params)) {
722
            return [];
723
        }
724
        $param = end($params);
725
        if (!$param->isDefaultValueAvailable()) {
726
            return [];
727
        }
728
        if (!$this->isAssoc($param->getDefaultValue())) {
729
            return [];
730
        }
731
        return $param->getDefaultValue();
732
    }
733
734
    /**
735
     * Helper; determine if an array is associative or not. An array
736
     * is not associative if its keys are numeric, and numbered sequentially
737
     * from zero. All other arrays are considered to be associative.
738
     *
739
     * @param array $arr The array
740
     * @return boolean
741
     */
742
    protected function isAssoc($arr)
743
    {
744
        if (!is_array($arr)) {
745
            return false;
746
        }
747
        return array_keys($arr) !== range(0, count($arr) - 1);
748
    }
749
750
    /**
751
     * Convert from a method name to the corresponding command name. A
752
     * method 'fooBar' will become 'foo:bar', and 'fooBarBazBoz' will
753
     * become 'foo:bar-baz-boz'.
754
     *
755
     * @param string $camel method name.
756
     * @return string
757
     */
758
    protected function convertName($camel)
759
    {
760
        $splitter="-";
761
        $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel));
762
        $camel = preg_replace("/$splitter/", ':', $camel, 1);
763
        return strtolower($camel);
764
    }
765
766
    /**
767
     * Parse the docBlock comment for this command, and set the
768
     * fields of this class with the data thereby obtained.
769
     */
770
    protected function parseDocBlock()
771
    {
772
        if (!$this->docBlockIsParsed) {
773
            // The parse function will insert data from the provided method
774
            // into this object, using our accessors.
775
            CommandDocBlockParserFactory::parse($this, $this->reflection);
776
            $this->docBlockIsParsed = true;
777
        }
778
    }
779
780
    /**
781
     * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
782
     * convert the data into the last of these forms.
783
     */
784
    protected static function convertListToCommaSeparated($text)
785
    {
786
        return preg_replace('#[ \t\n\r,]+#', ',', $text);
787
    }
788
}
789