Completed
Pull Request — master (#16)
by Greg
02:19
created

CommandInfo::setDefaultName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
namespace Consolidation\AnnotatedCommand;
3
4
/**
5
 * Given a class and method name, parse the annotations in the
6
 * DocBlock comment, and provide accessor methods for all of
7
 * the elements that are needed to create a Symfony Console Command.
8
 */
9
class CommandInfo
10
{
11
    /**
12
     * @var \ReflectionMethod
13
     */
14
    protected $reflection;
15
16
    /**
17
     * @var boolean
18
     * @var string
19
    */
20
    protected $docBlockIsParsed;
21
22
    /**
23
     * @var string
24
     */
25
    protected $name;
26
27
    /**
28
     * @var string
29
     */
30
    protected $description = '';
31
32
    /**
33
     * @var string
34
     */
35
    protected $help = '';
36
37
    /**
38
     * @var array
39
     */
40
    protected $options = [];
41
42
    /**
43
     * @var array
44
     */
45
    protected $arguments = [];
46
47
    /**
48
     * @var array
49
     */
50
    protected $argumentDescriptions = [];
51
52
    /**
53
     * @var array
54
     */
55
    protected $optionDescriptions = [];
56
57
    /**
58
     * @var array
59
     */
60
    protected $exampleUsage = [];
61
62
    /**
63
     * @var array
64
     */
65
    protected $otherAnnotations = [];
66
67
    /**
68
     * @var array
69
     */
70
    protected $aliases = [];
71
72
    /**
73
     * @var string
74
     */
75
    protected $methodName;
76
77
    /**
78
     * Create a new CommandInfo class for a particular method of a class.
79
     *
80
     * @param string|mixed $classNameOrInstance The name of a class, or an
81
     *   instance of it.
82
     * @param string $methodName The name of the method to get info about.
83
     */
84
    public function __construct($classNameOrInstance, $methodName)
85
    {
86
        $this->reflection = new \ReflectionMethod($classNameOrInstance, $methodName);
87
        $this->methodName = $methodName;
88
        // Set up a default name for the command from the method name.
89
        // This can be overridden via @command or @name annotations.
90
        $this->name = $this->convertName($this->reflection->name);
91
        $this->options = $this->determineOptionsFromParameters();
92
        $this->arguments = $this->determineAgumentClassifications();
93
    }
94
95
    /**
96
     * Recover the method name provided to the constructor.
97
     *
98
     * @return string
99
     */
100
    public function getMethodName()
101
    {
102
        return $this->methodName;
103
    }
104
105
    /**
106
     * Return the list of refleaction parameters.
107
     *
108
     * @return ReflectionParameter[]
109
     */
110
    public function getParameters()
111
    {
112
        return $this->reflection->getParameters();
113
    }
114
115
    /**
116
     * Get the synopsis of the command (~first line).
117
     *
118
     * @return string
119
     */
120
    public function getDescription()
121
    {
122
        $this->parseDocBlock();
123
        return $this->description;
124
    }
125
126
    /**
127
     * Set the command description.
128
     *
129
     * @param string $description The description to set.
130
     */
131
    public function setDescription($description)
132
    {
133
        $this->description = $description;
134
    }
135
136
    /**
137
     * Get the help text of the command (the description)
138
     */
139
    public function getHelp()
140
    {
141
        $this->parseDocBlock();
142
        return $this->help;
143
    }
144
    /**
145
     * Set the help text for this command.
146
     *
147
     * @param string $help The help text.
148
     */
149
    public function setHelp($help)
150
    {
151
        $this->help = $help;
152
    }
153
154
    /**
155
     * Return the list of aliases for this command.
156
     * @return string[]
157
     */
158
    public function getAliases()
159
    {
160
        $this->parseDocBlock();
161
        return $this->aliases;
162
    }
163
164
    /**
165
     * Set aliases that can be used in place of the command's primary name.
166
     *
167
     * @param string|string[] $aliases
168
     */
169
    public function setAliases($aliases)
170
    {
171
        if (is_string($aliases)) {
172
            $aliases = explode(',', static::convertListToCommaSeparated($aliases));
173
        }
174
        $this->aliases = array_filter($aliases);
175
    }
176
177
    /**
178
     * Return the examples for this command. This is @usage instead of
179
     * @example because the later is defined by the phpdoc standard to
180
     * be example method calls.
181
     *
182
     * @return string[]
183
     */
184
    public function getExampleUsages()
185
    {
186
        $this->parseDocBlock();
187
        return $this->exampleUsage;
188
    }
189
190
    /**
191
     * Return the primary name for this command.
192
     *
193
     * @return string
194
     */
195
    public function getName()
196
    {
197
        $this->parseDocBlock();
198
        return $this->name;
199
    }
200
201
    /**
202
     * Set the primary name for this command.
203
     *
204
     * @param string $name
205
     */
206
    public function setName($name)
207
    {
208
        $this->name = $name;
209
    }
210
211
    /**
212
     * Examine the parameters of the method for this command, and
213
     * build a list of commandline arguements for them.
214
     *
215
     * @return array
216
     */
217
    protected function determineAgumentClassifications()
218
    {
219
        $args = [];
220
        $params = $this->reflection->getParameters();
221
        if (!empty($this->determineOptionsFromParameters())) {
222
            array_pop($params);
223
        }
224
        foreach ($params as $param) {
225
            $defaultValue = $this->getArgumentClassification($param);
226
            if ($defaultValue !== false) {
227
                $args[$param->name] = $defaultValue;
228
            }
229
        }
230
        return $args;
231
    }
232
233
    /**
234
     * Return the commandline arguments for this command. The key
235
     * contains the name of the argument, and the value contains its
236
     * default value. Required commands have a 'null' value.
237
     *
238
     * @return array
239
     */
240
    public function getArguments()
241
    {
242
        return $this->arguments;
243
    }
244
245
    /**
246
     * Check to see if an argument with the specified name exits.
247
     *
248
     * @param string $name Argument to test for.
249
     * @return boolean
250
     */
251
    public function hasArgument($name)
252
    {
253
        return array_key_exists($name, $this->arguments);
254
    }
255
256
    /**
257
     * Set the default value for an argument. A default value of 'null'
258
     * indicates that the argument is required.
259
     *
260
     * @param string $name Name of argument to modify.
261
     * @param string $defaultValue New default value for that argument.
262
     */
263
    public function setArgumentDefaultValue($name, $defaultValue)
264
    {
265
        $this->arguments[$name] = $defaultValue;
266
    }
267
268
    /**
269
     * Add another argument to this command.
270
     *
271
     * @param string $name Name of the argument.
272
     * @param string $description Help text for the argument.
273
     * @param string $defaultValue The default value for the argument.
274
     */
275
    public function addArgument($name, $description, $defaultValue = null)
276
    {
277
        if (!$this->hasArgument($name) || isset($defaultValue)) {
278
            $this->arguments[$name] = $defaultValue;
279
        }
280
        unset($this->argumentDescriptions[$name]);
281
        if (isset($description)) {
282
            $this->argumentDescriptions[$name] = $description;
283
        }
284
    }
285
286
    /**
287
     * Examine the provided parameter, and determine whether it
288
     * is a parameter that will be filled in with a positional
289
     * commandline argument.
290
     *
291
     * @return false|null|string|array
292
     */
293
    protected function getArgumentClassification($param)
294
    {
295
        $defaultValue = null;
296
        if ($param->isDefaultValueAvailable()) {
297
            $defaultValue = $param->getDefaultValue();
298
            if ($this->isAssoc($defaultValue)) {
299
                return false;
300
            }
301
        }
302
        if ($param->isArray()) {
303
            return [];
304
        }
305
        // Commandline arguments must be strings, so ignore
306
        // any parameter that is typehinted to anything else.
307
        if (($param->getClass() != null) && ($param->getClass() != 'string')) {
308
            return false;
309
        }
310
        return $defaultValue;
311
    }
312
313
    /**
314
     * Return the options for is command. The key is the options name,
315
     * and the value is its default value.
316
     *
317
     * @return array
318
     */
319
    public function getOptions()
320
    {
321
        return $this->options;
322
    }
323
324
    /**
325
     * Check to see if the specified option exists.
326
     *
327
     * @param string $name Name of the option to check.
328
     * @return boolean
329
     */
330
    public function hasOption($name)
331
    {
332
        return array_key_exists($name, $this->options);
333
    }
334
335
    /**
336
     * Change the default value for an option.
337
     *
338
     * @param string $name Option name.
339
     * @param string $defaultValue Option default value.
340
     */
341
    public function setOptionDefaultValue($name, $defaultValue)
342
    {
343
        $this->options[$name] = $defaultValue;
344
    }
345
346
    /**
347
     * Add another option to this command.
348
     *
349
     * @param string $name Option name.
350
     * @param string $description Option description.
351
     * @param string $defaultValue Option default value.
352
     */
353
    public function addOption($name, $description, $defaultValue = false)
354
    {
355
        if (!$this->hasOption($name) || ($defaultValue !== false)) {
356
            $this->options[$name] = $defaultValue;
357
        }
358
        unset($this->optionDescriptions[$name]);
359
        if (isset($description)) {
360
            $this->optionDescriptions[$name] = $description;
361
        }
362
    }
363
364
    /**
365
     * Examine the parameters of the method for this command, and determine
366
     * the disposition of the options from them.
367
     *
368
     * @return array
369
     */
370
    public function determineOptionsFromParameters()
371
    {
372
        $params = $this->reflection->getParameters();
373
        if (empty($params)) {
374
            return [];
375
        }
376
        $param = end($params);
377
        if (!$param->isDefaultValueAvailable()) {
378
            return [];
379
        }
380
        if (!$this->isAssoc($param->getDefaultValue())) {
381
            return [];
382
        }
383
        return $param->getDefaultValue();
384
    }
385
386
    /**
387
     * Get the description of one argument.
388
     *
389
     * @param string $name The name of the argument.
390
     * @return string
391
     */
392
    public function getArgumentDescription($name)
393
    {
394
        $this->parseDocBlock();
395
        if (array_key_exists($name, $this->argumentDescriptions)) {
396
            return $this->argumentDescriptions[$name];
397
        }
398
399
        return '';
400
    }
401
402
    /**
403
     * Get the description of one argument.
404
     *
405
     * @param string $name The name of the option.
406
     * @return string
407
     */
408
    public function getOptionDescription($name)
409
    {
410
        $this->parseDocBlock();
411
        if (array_key_exists($name, $this->optionDescriptions)) {
412
            return $this->optionDescriptions[$name];
413
        }
414
415
        return '';
416
    }
417
418
    /**
419
     * Helper; determine if an array is associative or not. An array
420
     * is not associative if its keys are numeric, and numbered sequentially
421
     * from zero. All other arrays are considered to be associative.
422
     *
423
     * @param arrau $arr The array
424
     * @return boolean
425
     */
426
    protected function isAssoc($arr)
427
    {
428
        if (!is_array($arr)) {
429
            return false;
430
        }
431
        return array_keys($arr) !== range(0, count($arr) - 1);
432
    }
433
434
    /**
435
     * Get any annotations included in the docblock comment for the
436
     * implementation method of this command that are not already
437
     * handled by the primary methods of this class.
438
     *
439
     * @return array
440
     */
441
    public function getAnnotations()
442
    {
443
        $this->parseDocBlock();
444
        return $this->otherAnnotations;
445
    }
446
447
    /**
448
     * Return a specific named annotation for this command.
449
     *
450
     * @param string $annotation The name of the annotation.
451
     * @return string
452
     */
453
    public function getAnnotation($annotation)
454
    {
455
        // hasAnnotation parses the docblock
456
        if (!$this->hasAnnotation($annotation)) {
457
            return null;
458
        }
459
        return $this->otherAnnotations[$annotation];
460
    }
461
462
    /**
463
     * Check to see if the specified annotation exists for this command.
464
     *
465
     * @param string $annotation The name of the annotation.
466
     * @return boolean
467
     */
468
    public function hasAnnotation($annotation)
469
    {
470
        $this->parseDocBlock();
471
        return array_key_exists($annotation, $this->otherAnnotations);
472
    }
473
474
    /**
475
     * Convert from a method name to the corresponding command name. A
476
     * method 'fooBar' will become 'foo:bar', and 'fooBarBazBoz' will
477
     * become 'foo:bar-baz-boz'.
478
     *
479
     * @param type $camel method name.
480
     * @return string
481
     */
482
    protected function convertName($camel)
483
    {
484
        $splitter="-";
485
        $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel));
486
        $camel = preg_replace("/$splitter/", ':', $camel, 1);
487
        return strtolower($camel);
488
    }
489
490
    /**
491
     * Add an example usage for this command.
492
     *
493
     * @param string $usage An example of the command, including the command
494
     *   name and all of its example arguments and options.
495
     * @param string $description An explanation of what the example does.
496
     */
497
    public function setExampleUsage($usage, $description)
498
    {
499
        $this->exampleUsage[$usage] = $description;
500
    }
501
502
    /**
503
     * Parse the docBlock comment for this command, and set the
504
     * fields of this class with the data thereby obtained.
505
     */
506
    protected function parseDocBlock()
507
    {
508
        if (!$this->docBlockIsParsed) {
509
            $docblock = $this->reflection->getDocComment();
510
            $parser = new CommandDocBlockParser($this);
511
            $parser->parse($docblock);
512
            $this->docBlockIsParsed = true;
513
        }
514
    }
515
516
    /**
517
     * Save any tag that we do not explicitly recognize in the
518
     * 'otherAnnotations' map.
519
     */
520
    public function addOtherAnnotation($name, $content)
521
    {
522
        $this->otherAnnotations[$name] = $content;
523
    }
524
525
    /**
526
     * An option might have a name such as 'silent|s'. In this
527
     * instance, we will allow the @option or @default tag to
528
     * reference the option only by name (e.g. 'silent' or 's'
529
     * instead of 'silent|s').
530
     */
531
    public function findMatchingOption($optionName)
532
    {
533
        // Exit fast if there's an exact match
534
        if (isset($this->options[$optionName])) {
535
            return $optionName;
536
        }
537
        // Check to see if we can find the option name in an existing option,
538
        // e.g. if the options array has 'silent|s' => false, and the annotation
539
        // is @silent.
540
        foreach ($this->options as $name => $default) {
541
            if (in_array($optionName, explode('|', $name))) {
542
                return $name;
543
            }
544
        }
545
        // Check the other direction: if the annotation contains @silent|s
546
        // and the options array has 'silent|s'.
547
        $checkMatching = explode('|', $optionName);
548
        if (count($checkMatching) > 1) {
549
            foreach ($checkMatching as $checkName) {
550
                if (isset($this->options[$checkName])) {
551
                    $this->options[$optionName] = $this->options[$checkName];
552
                    unset($this->options[$checkName]);
553
                    return $optionName;
554
                }
555
            }
556
        }
557
        return $optionName;
558
    }
559
560
    /**
561
     * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
562
     * convert the data into the last of these forms.
563
     */
564
    protected static function convertListToCommaSeparated($text)
565
    {
566
        return preg_replace('#[ \t\n\r,]+#', ',', $text);
567
    }
568
}
569