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

CommandInfo::getHelp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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