CommandInfo   F
last analyzed

Complexity

Total Complexity 93

Size/Duplication

Total Lines 769
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 93
lcom 1
cbo 5
dl 0
loc 769
rs 1.831
c 0
b 0
f 0

51 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 4
A create() 0 4 1
A deserialize() 0 5 1
A cachedFileIsModified() 0 5 1
A constructFromClassAndMethod() 0 9 1
A getMethodName() 0 4 1
A getName() 0 5 1
A setName() 0 5 1
A valid() 0 4 1
A invalidate() 0 4 1
A getReturnType() 0 5 1
A getInjectedClasses() 0 5 1
A setInjectedClasses() 0 5 1
A setReturnType() 0 5 1
A getRawAnnotations() 0 5 1
A replaceRawAnnotations() 0 5 1
A getAnnotations() 0 15 1
A getAnnotationList() 0 9 2
A getAnnotation() 0 8 2
A hasAnnotation() 0 5 1
A addAnnotation() 0 9 2
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 getHidden() 0 5 1
A setHidden() 0 5 1
A getExampleUsages() 0 5 1
A setExampleUsage() 0 5 1
A replaceExampleUsages() 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
A addImplicitNoOptions() 0 13 4
B createInputOptions() 0 44 9
A findMatchingOption() 0 12 3
A findOptionAmongAlternatives() 0 15 4
A findExistingOption() 0 11 3
A determineAgumentClassifications() 0 18 5
A addParameterToResult() 0 17 5
A determineOptionsFromParameters() 0 15 4
A lastParameterIsOptionsArray() 0 12 3
A isAssoc() 0 7 2
A convertName() 0 7 1
A parseDocBlock() 0 9 2
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\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 = 4;
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
     * @var string[]
93
     */
94
    protected $injectedClasses = [];
95
96
    /**
97
     * Create a new CommandInfo class for a particular method of a class.
98
     *
99
     * @param string|mixed $classNameOrInstance The name of a class, or an
100
     *   instance of it, or an array of cached data.
101
     * @param string $methodName The name of the method to get info about.
102
     * @param array $cache Cached data
103
     * @deprecated Use CommandInfo::create() or CommandInfo::deserialize()
104
     *   instead. In the future, this constructor will be protected.
105
     */
106
    public function __construct($classNameOrInstance, $methodName, $cache = [])
107
    {
108
        $this->reflection = new \ReflectionMethod($classNameOrInstance, $methodName);
109
        $this->methodName = $methodName;
110
        $this->arguments = new DefaultsWithDescriptions();
111
        $this->options = new DefaultsWithDescriptions();
112
113
        // If the cache came from a newer version, ignore it and
114
        // regenerate the cached information.
115
        if (!empty($cache) && CommandInfoDeserializer::isValidSerializedData($cache) && !$this->cachedFileIsModified($cache)) {
116
            $deserializer = new CommandInfoDeserializer();
117
            $deserializer->constructFromCache($this, $cache);
118
            $this->docBlockIsParsed = true;
119
        } else {
120
            $this->constructFromClassAndMethod($classNameOrInstance, $methodName);
121
        }
122
    }
123
124
    public static function create($classNameOrInstance, $methodName)
125
    {
126
        return new self($classNameOrInstance, $methodName);
127
    }
128
129
    public static function deserialize($cache)
130
    {
131
        $cache = (array)$cache;
132
        return new self($cache['class'], $cache['method_name'], $cache);
133
    }
134
135
    public function cachedFileIsModified($cache)
136
    {
137
        $path = $this->reflection->getFileName();
138
        return filemtime($path) != $cache['mtime'];
139
    }
140
141
    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...
142
    {
143
        $this->otherAnnotations = new AnnotationData();
144
        // Set up a default name for the command from the method name.
145
        // This can be overridden via @command or @name annotations.
146
        $this->name = $this->convertName($methodName);
147
        $this->options = new DefaultsWithDescriptions($this->determineOptionsFromParameters(), false);
148
        $this->arguments = $this->determineAgumentClassifications();
149
    }
150
151
    /**
152
     * Recover the method name provided to the constructor.
153
     *
154
     * @return string
155
     */
156
    public function getMethodName()
157
    {
158
        return $this->methodName;
159
    }
160
161
    /**
162
     * Return the primary name for this command.
163
     *
164
     * @return string
165
     */
166
    public function getName()
167
    {
168
        $this->parseDocBlock();
169
        return $this->name;
170
    }
171
172
    /**
173
     * Set the primary name for this command.
174
     *
175
     * @param string $name
176
     */
177
    public function setName($name)
178
    {
179
        $this->name = $name;
180
        return $this;
181
    }
182
183
    /**
184
     * Return whether or not this method represents a valid command
185
     * or hook.
186
     */
187
    public function valid()
188
    {
189
        return !empty($this->name);
190
    }
191
192
    /**
193
     * If higher-level code decides that this CommandInfo is not interesting
194
     * or useful (if it is not a command method or a hook method), then
195
     * we will mark it as invalid to prevent it from being created as a command.
196
     * We still cache a placeholder record for invalid methods, so that we
197
     * do not need to re-parse the method again later simply to determine that
198
     * it is invalid.
199
     */
200
    public function invalidate()
201
    {
202
        $this->name = '';
203
    }
204
205
    public function getReturnType()
206
    {
207
        $this->parseDocBlock();
208
        return $this->returnType;
209
    }
210
211
    public function getInjectedClasses()
212
    {
213
        $this->parseDocBlock();
214
        return $this->injectedClasses;
215
    }
216
217
    public function setInjectedClasses($injectedClasses)
218
    {
219
        $this->injectedClasses = $injectedClasses;
220
        return $this;
221
    }
222
223
    public function setReturnType($returnType)
224
    {
225
        $this->returnType = $returnType;
226
        return $this;
227
    }
228
229
    /**
230
     * Get any annotations included in the docblock comment for the
231
     * implementation method of this command that are not already
232
     * handled by the primary methods of this class.
233
     *
234
     * @return AnnotationData
235
     */
236
    public function getRawAnnotations()
237
    {
238
        $this->parseDocBlock();
239
        return $this->otherAnnotations;
240
    }
241
242
    /**
243
     * Replace the annotation data.
244
     */
245
    public function replaceRawAnnotations($annotationData)
246
    {
247
        $this->otherAnnotations = new AnnotationData((array) $annotationData);
248
        return $this;
249
    }
250
251
    /**
252
     * Get any annotations included in the docblock comment,
253
     * also including default values such as @command.  We add
254
     * in the default @command annotation late, and only in a
255
     * copy of the annotation data because we use the existance
256
     * of a @command to indicate that this CommandInfo is
257
     * a command, and not a hook or anything else.
258
     *
259
     * @return AnnotationData
260
     */
261
    public function getAnnotations()
262
    {
263
        // Also provide the path to the commandfile that these annotations
264
        // were pulled from and the classname of that file.
265
        $path = $this->reflection->getFileName();
266
        $className = $this->reflection->getDeclaringClass()->getName();
0 ignored issues
show
introduced by
Consider using $this->reflection->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
267
        return new AnnotationData(
268
            $this->getRawAnnotations()->getArrayCopy() +
269
            [
270
                'command' => $this->getName(),
271
                '_path' => $path,
272
                '_classname' => $className,
273
            ]
274
        );
275
    }
276
277
    /**
278
     * Return a specific named annotation for this command as a list.
279
     *
280
     * @param string $name The name of the annotation.
281
     * @return array|null
282
     */
283
    public function getAnnotationList($name)
284
    {
285
        // hasAnnotation parses the docblock
286
        if (!$this->hasAnnotation($name)) {
287
            return null;
288
        }
289
        return $this->otherAnnotations->getList($name);
290
        ;
291
    }
292
293
    /**
294
     * Return a specific named annotation for this command as a string.
295
     *
296
     * @param string $name The name of the annotation.
297
     * @return string|null
298
     */
299
    public function getAnnotation($name)
300
    {
301
        // hasAnnotation parses the docblock
302
        if (!$this->hasAnnotation($name)) {
303
            return null;
304
        }
305
        return $this->otherAnnotations->get($name);
306
    }
307
308
    /**
309
     * Check to see if the specified annotation exists for this command.
310
     *
311
     * @param string $annotation The name of the annotation.
312
     * @return boolean
313
     */
314
    public function hasAnnotation($annotation)
315
    {
316
        $this->parseDocBlock();
317
        return isset($this->otherAnnotations[$annotation]);
318
    }
319
320
    /**
321
     * Save any tag that we do not explicitly recognize in the
322
     * 'otherAnnotations' map.
323
     */
324
    public function addAnnotation($name, $content)
325
    {
326
        // Convert to an array and merge if there are multiple
327
        // instances of the same annotation defined.
328
        if (isset($this->otherAnnotations[$name])) {
329
            $content = array_merge((array) $this->otherAnnotations[$name], (array)$content);
330
        }
331
        $this->otherAnnotations[$name] = $content;
332
    }
333
334
    /**
335
     * Remove an annotation that was previoudly set.
336
     */
337
    public function removeAnnotation($name)
338
    {
339
        unset($this->otherAnnotations[$name]);
340
    }
341
342
    /**
343
     * Get the synopsis of the command (~first line).
344
     *
345
     * @return string
346
     */
347
    public function getDescription()
348
    {
349
        $this->parseDocBlock();
350
        return $this->description;
351
    }
352
353
    /**
354
     * Set the command description.
355
     *
356
     * @param string $description The description to set.
357
     */
358
    public function setDescription($description)
359
    {
360
        $this->description = str_replace("\n", ' ', $description);
361
        return $this;
362
    }
363
364
    /**
365
     * Get the help text of the command (the description)
366
     */
367
    public function getHelp()
368
    {
369
        $this->parseDocBlock();
370
        return $this->help;
371
    }
372
    /**
373
     * Set the help text for this command.
374
     *
375
     * @param string $help The help text.
376
     */
377
    public function setHelp($help)
378
    {
379
        $this->help = $help;
380
        return $this;
381
    }
382
383
    /**
384
     * Return the list of aliases for this command.
385
     * @return string[]
386
     */
387
    public function getAliases()
388
    {
389
        $this->parseDocBlock();
390
        return $this->aliases;
391
    }
392
393
    /**
394
     * Set aliases that can be used in place of the command's primary name.
395
     *
396
     * @param string|string[] $aliases
397
     */
398
    public function setAliases($aliases)
399
    {
400
        if (is_string($aliases)) {
401
            $aliases = explode(',', static::convertListToCommaSeparated($aliases));
402
        }
403
        $this->aliases = array_filter($aliases);
404
        return $this;
405
    }
406
407
    /**
408
     * Get hidden status for the command.
409
     * @return bool
410
     */
411
    public function getHidden()
412
    {
413
        $this->parseDocBlock();
414
        return $this->hasAnnotation('hidden');
415
    }
416
417
    /**
418
     * Set hidden status. List command omits hidden commands.
419
     *
420
     * @param bool $hidden
421
     */
422
    public function setHidden($hidden)
423
    {
424
        $this->hidden = $hidden;
0 ignored issues
show
Bug introduced by
The property hidden does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
425
        return $this;
426
    }
427
428
    /**
429
     * Return the examples for this command. This is @usage instead of
430
     * @example because the later is defined by the phpdoc standard to
431
     * be example method calls.
432
     *
433
     * @return string[]
434
     */
435
    public function getExampleUsages()
436
    {
437
        $this->parseDocBlock();
438
        return $this->exampleUsage;
439
    }
440
441
    /**
442
     * Add an example usage for this command.
443
     *
444
     * @param string $usage An example of the command, including the command
445
     *   name and all of its example arguments and options.
446
     * @param string $description An explanation of what the example does.
447
     */
448
    public function setExampleUsage($usage, $description)
449
    {
450
        $this->exampleUsage[$usage] = $description;
451
        return $this;
452
    }
453
454
    /**
455
     * Overwrite all example usages
456
     */
457
    public function replaceExampleUsages($usages)
458
    {
459
        $this->exampleUsage = $usages;
460
        return $this;
461
    }
462
463
    /**
464
     * Return the topics for this command.
465
     *
466
     * @return string[]
467
     */
468
    public function getTopics()
469
    {
470
        if (!$this->hasAnnotation('topics')) {
471
            return [];
472
        }
473
        $topics = $this->getAnnotation('topics');
474
        return explode(',', trim($topics));
475
    }
476
477
    /**
478
     * Return the list of refleaction parameters.
479
     *
480
     * @return ReflectionParameter[]
481
     */
482
    public function getParameters()
483
    {
484
        return $this->reflection->getParameters();
485
    }
486
487
    /**
488
     * Descriptions of commandline arguements for this command.
489
     *
490
     * @return DefaultsWithDescriptions
491
     */
492
    public function arguments()
493
    {
494
        return $this->arguments;
495
    }
496
497
    /**
498
     * Descriptions of commandline options for this command.
499
     *
500
     * @return DefaultsWithDescriptions
501
     */
502
    public function options()
503
    {
504
        return $this->options;
505
    }
506
507
    /**
508
     * Get the inputOptions for the options associated with this CommandInfo
509
     * object, e.g. via @option annotations, or from
510
     * $options = ['someoption' => 'defaultvalue'] in the command method
511
     * parameter list.
512
     *
513
     * @return InputOption[]
514
     */
515
    public function inputOptions()
516
    {
517
        if (!isset($this->inputOptions)) {
518
            $this->inputOptions = $this->createInputOptions();
519
        }
520
        return $this->inputOptions;
521
    }
522
523
    protected function addImplicitNoOptions()
524
    {
525
        $opts = $this->options()->getValues();
526
        foreach ($opts as $name => $defaultValue) {
527
            if ($defaultValue === true) {
528
                $key = 'no-' . $name;
529
                if (!array_key_exists($key, $opts)) {
530
                    $description = "Negate --$name option.";
531
                    $this->options()->add($key, $description, false);
532
                }
533
            }
534
        }
535
    }
536
537
    protected function createInputOptions()
538
    {
539
        $explicitOptions = [];
540
        $this->addImplicitNoOptions();
541
542
        $opts = $this->options()->getValues();
543
        foreach ($opts as $name => $defaultValue) {
544
            $description = $this->options()->getDescription($name);
545
546
            $fullName = $name;
547
            $shortcut = '';
548
            if (strpos($name, '|')) {
549
                list($fullName, $shortcut) = explode('|', $name, 2);
550
            }
551
552
            // Treat the following two cases identically:
553
            //   - 'foo' => InputOption::VALUE_OPTIONAL
554
            //   - 'foo' => null
555
            // The first form is preferred, but we will convert the value
556
            // to 'null' for storage as the option default value.
557
            if ($defaultValue === InputOption::VALUE_OPTIONAL) {
558
                $defaultValue = null;
559
            }
560
561
            if ($defaultValue === false) {
562
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_NONE, $description);
563
            } elseif ($defaultValue === InputOption::VALUE_REQUIRED) {
564
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_REQUIRED, $description);
565
            } elseif (is_array($defaultValue)) {
566
                $optionality = count($defaultValue) ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_REQUIRED;
567
                $explicitOptions[$fullName] = new InputOption(
568
                    $fullName,
569
                    $shortcut,
570
                    InputOption::VALUE_IS_ARRAY | $optionality,
571
                    $description,
572
                    count($defaultValue) ? $defaultValue : null
0 ignored issues
show
Bug introduced by
It seems like count($defaultValue) ? $defaultValue : null can also be of type array; however, Symfony\Component\Consol...utOption::__construct() does only seem to accept string|array<integer,string>|integer|boolean|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
573
                );
574
            } else {
575
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $defaultValue);
576
            }
577
        }
578
579
        return $explicitOptions;
580
    }
581
582
    /**
583
     * An option might have a name such as 'silent|s'. In this
584
     * instance, we will allow the @option or @default tag to
585
     * reference the option only by name (e.g. 'silent' or 's'
586
     * instead of 'silent|s').
587
     *
588
     * @param string $optionName
589
     * @return string
590
     */
591
    public function findMatchingOption($optionName)
592
    {
593
        // Exit fast if there's an exact match
594
        if ($this->options->exists($optionName)) {
595
            return $optionName;
596
        }
597
        $existingOptionName = $this->findExistingOption($optionName);
598
        if (isset($existingOptionName)) {
599
            return $existingOptionName;
600
        }
601
        return $this->findOptionAmongAlternatives($optionName);
602
    }
603
604
    /**
605
     * @param string $optionName
606
     * @return string
607
     */
608
    protected function findOptionAmongAlternatives($optionName)
609
    {
610
        // Check the other direction: if the annotation contains @silent|s
611
        // and the options array has 'silent|s'.
612
        $checkMatching = explode('|', $optionName);
613
        if (count($checkMatching) > 1) {
614
            foreach ($checkMatching as $checkName) {
615
                if ($this->options->exists($checkName)) {
616
                    $this->options->rename($checkName, $optionName);
617
                    return $optionName;
618
                }
619
            }
620
        }
621
        return $optionName;
622
    }
623
624
    /**
625
     * @param string $optionName
626
     * @return string|null
627
     */
628
    protected function findExistingOption($optionName)
629
    {
630
        // Check to see if we can find the option name in an existing option,
631
        // e.g. if the options array has 'silent|s' => false, and the annotation
632
        // is @silent.
633
        foreach ($this->options()->getValues() as $name => $default) {
634
            if (in_array($optionName, explode('|', $name))) {
635
                return $name;
636
            }
637
        }
638
    }
639
640
    /**
641
     * Examine the parameters of the method for this command, and
642
     * build a list of commandline arguements for them.
643
     *
644
     * @return array
645
     */
646
    protected function determineAgumentClassifications()
647
    {
648
        $result = new DefaultsWithDescriptions();
649
        $params = $this->reflection->getParameters();
650
        $optionsFromParameters = $this->determineOptionsFromParameters();
0 ignored issues
show
Unused Code introduced by
$optionsFromParameters is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
651
        if ($this->lastParameterIsOptionsArray()) {
652
            array_pop($params);
653
        }
654
        while (!empty($params) && ($params[0]->getClass() != null)) {
655
            $param = array_shift($params);
656
            $injectedClass = $param->getClass()->getName();
657
            array_unshift($this->injectedClasses, $injectedClass);
658
        }
659
        foreach ($params as $param) {
660
            $this->addParameterToResult($result, $param);
661
        }
662
        return $result;
663
    }
664
665
    /**
666
     * Examine the provided parameter, and determine whether it
667
     * is a parameter that will be filled in with a positional
668
     * commandline argument.
669
     */
670
    protected function addParameterToResult($result, $param)
671
    {
672
        // Commandline arguments must be strings, so ignore any
673
        // parameter that is typehinted to any non-primative class.
674
        if ($param->getClass() != null) {
675
            return;
676
        }
677
        $result->add($param->name);
678
        if ($param->isDefaultValueAvailable()) {
679
            $defaultValue = $param->getDefaultValue();
680
            if (!$this->isAssoc($defaultValue)) {
681
                $result->setDefaultValue($param->name, $defaultValue);
682
            }
683
        } elseif ($param->isArray()) {
684
            $result->setDefaultValue($param->name, []);
685
        }
686
    }
687
688
    /**
689
     * Examine the parameters of the method for this command, and determine
690
     * the disposition of the options from them.
691
     *
692
     * @return array
693
     */
694
    protected function determineOptionsFromParameters()
695
    {
696
        $params = $this->reflection->getParameters();
697
        if (empty($params)) {
698
            return [];
699
        }
700
        $param = end($params);
701
        if (!$param->isDefaultValueAvailable()) {
702
            return [];
703
        }
704
        if (!$this->isAssoc($param->getDefaultValue())) {
705
            return [];
706
        }
707
        return $param->getDefaultValue();
708
    }
709
710
    /**
711
     * Determine if the last argument contains $options.
712
     *
713
     * Two forms indicate options:
714
     * - $options = []
715
     * - $options = ['flag' => 'default-value']
716
     *
717
     * Any other form, including `array $foo`, is not options.
718
     */
719
    protected function lastParameterIsOptionsArray()
720
    {
721
        $params = $this->reflection->getParameters();
722
        if (empty($params)) {
723
            return [];
724
        }
725
        $param = end($params);
726
        if (!$param->isDefaultValueAvailable()) {
727
            return [];
728
        }
729
        return is_array($param->getDefaultValue());
730
    }
731
732
    /**
733
     * Helper; determine if an array is associative or not. An array
734
     * is not associative if its keys are numeric, and numbered sequentially
735
     * from zero. All other arrays are considered to be associative.
736
     *
737
     * @param array $arr The array
738
     * @return boolean
739
     */
740
    protected function isAssoc($arr)
741
    {
742
        if (!is_array($arr)) {
743
            return false;
744
        }
745
        return array_keys($arr) !== range(0, count($arr) - 1);
746
    }
747
748
    /**
749
     * Convert from a method name to the corresponding command name. A
750
     * method 'fooBar' will become 'foo:bar', and 'fooBarBazBoz' will
751
     * become 'foo:bar-baz-boz'.
752
     *
753
     * @param string $camel method name.
754
     * @return string
755
     */
756
    protected function convertName($camel)
757
    {
758
        $splitter="-";
759
        $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel));
760
        $camel = preg_replace("/$splitter/", ':', $camel, 1);
761
        return strtolower($camel);
762
    }
763
764
    /**
765
     * Parse the docBlock comment for this command, and set the
766
     * fields of this class with the data thereby obtained.
767
     */
768
    protected function parseDocBlock()
769
    {
770
        if (!$this->docBlockIsParsed) {
771
            // The parse function will insert data from the provided method
772
            // into this object, using our accessors.
773
            CommandDocBlockParserFactory::parse($this, $this->reflection);
774
            $this->docBlockIsParsed = true;
775
        }
776
    }
777
778
    /**
779
     * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
780
     * convert the data into the last of these forms.
781
     */
782
    protected static function convertListToCommaSeparated($text)
783
    {
784
        return preg_replace('#[ \t\n\r,]+#', ',', $text);
785
    }
786
}
787