Completed
Pull Request — master (#78)
by
unknown
02:23
created

CommandInfo::setAliases()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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