Completed
Pull Request — master (#19)
by Greg
02:19
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\Parser;
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
     * Descriptions of commandline arguements for this command.
264
     *
265
     * @return DefaultsWithDescriptions
266
     */
267
    public function arguments()
268
    {
269
        return $this->arguments;
270
    }
271
272
    /**
273
     * Descriptions of commandline options for this command.
274
     *
275
     * @return DefaultsWithDescriptions
276
     */
277
    public function options()
278
    {
279
        return $this->options;
280
    }
281
282
    /**
283
     * An option might have a name such as 'silent|s'. In this
284
     * instance, we will allow the @option or @default tag to
285
     * reference the option only by name (e.g. 'silent' or 's'
286
     * instead of 'silent|s').
287
     *
288
     * @param string $optionName
289
     * @return string
290
     */
291
    public function findMatchingOption($optionName)
292
    {
293
        // Exit fast if there's an exact match
294
        if ($this->options->exists($optionName)) {
295
            return $optionName;
296
        }
297
        $existingOptionName = $this->findExistingOption($optionName);
298
        if (isset($existingOptionName)) {
299
            return $existingOptionName;
300
        }
301
        return $this->findOptionAmongAlternatives($optionName);
302
    }
303
304
    /**
305
     * @param string $optionName
306
     * @return string
307
     */
308
    protected function findOptionAmongAlternatives($optionName)
309
    {
310
        // Check the other direction: if the annotation contains @silent|s
311
        // and the options array has 'silent|s'.
312
        $checkMatching = explode('|', $optionName);
313
        if (count($checkMatching) > 1) {
314
            foreach ($checkMatching as $checkName) {
315
                if ($this->options->exists($checkName)) {
316
                    $this->options->rename($checkName, $optionName);
317
                    return $optionName;
318
                }
319
            }
320
        }
321
        return $optionName;
322
    }
323
324
    /**
325
     * @param string $optionName
326
     * @return string|null
327
     */
328
    protected function findExistingOption($optionName)
329
    {
330
        // Check to see if we can find the option name in an existing option,
331
        // e.g. if the options array has 'silent|s' => false, and the annotation
332
        // is @silent.
333
        foreach ($this->options()->getValues() as $name => $default) {
334
            if (in_array($optionName, explode('|', $name))) {
335
                return $name;
336
            }
337
        }
338
    }
339
340
    /**
341
     * Examine the parameters of the method for this command, and
342
     * build a list of commandline arguements for them.
343
     *
344
     * @return array
345
     */
346
    protected function determineAgumentClassifications()
347
    {
348
        $args = [];
349
        $params = $this->reflection->getParameters();
350
        if (!empty($this->determineOptionsFromParameters())) {
351
            array_pop($params);
352
        }
353
        foreach ($params as $param) {
354
            $defaultValue = $this->getArgumentClassification($param);
355
            if ($defaultValue !== false) {
356
                $args[$param->name] = $defaultValue;
357
            }
358
        }
359
        return $args;
360
    }
361
362
    /**
363
     * Examine the provided parameter, and determine whether it
364
     * is a parameter that will be filled in with a positional
365
     * commandline argument.
366
     *
367
     * @return false|null|string|array
368
     */
369
    protected function getArgumentClassification($param)
370
    {
371
        $defaultValue = null;
372
        if ($param->isDefaultValueAvailable()) {
373
            $defaultValue = $param->getDefaultValue();
374
            if ($this->isAssoc($defaultValue)) {
375
                return false;
376
            }
377
        }
378
        if ($param->isArray()) {
379
            return [];
380
        }
381
        // Commandline arguments must be strings, so ignore
382
        // any parameter that is typehinted to anything else.
383
        if (($param->getClass() != null) && ($param->getClass() != 'string')) {
384
            return false;
385
        }
386
        return $defaultValue;
387
    }
388
389
    /**
390
     * Examine the parameters of the method for this command, and determine
391
     * the disposition of the options from them.
392
     *
393
     * @return array
394
     */
395
    protected function determineOptionsFromParameters()
396
    {
397
        $params = $this->reflection->getParameters();
398
        if (empty($params)) {
399
            return [];
400
        }
401
        $param = end($params);
402
        if (!$param->isDefaultValueAvailable()) {
403
            return [];
404
        }
405
        if (!$this->isAssoc($param->getDefaultValue())) {
406
            return [];
407
        }
408
        return $param->getDefaultValue();
409
    }
410
411
    /**
412
     * Helper; determine if an array is associative or not. An array
413
     * is not associative if its keys are numeric, and numbered sequentially
414
     * from zero. All other arrays are considered to be associative.
415
     *
416
     * @param arrau $arr The array
417
     * @return boolean
418
     */
419
    protected function isAssoc($arr)
420
    {
421
        if (!is_array($arr)) {
422
            return false;
423
        }
424
        return array_keys($arr) !== range(0, count($arr) - 1);
425
    }
426
427
    /**
428
     * Convert from a method name to the corresponding command name. A
429
     * method 'fooBar' will become 'foo:bar', and 'fooBarBazBoz' will
430
     * become 'foo:bar-baz-boz'.
431
     *
432
     * @param string $camel method name.
433
     * @return string
434
     */
435
    protected function convertName($camel)
436
    {
437
        $splitter="-";
438
        $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel));
439
        $camel = preg_replace("/$splitter/", ':', $camel, 1);
440
        return strtolower($camel);
441
    }
442
443
    /**
444
     * Parse the docBlock comment for this command, and set the
445
     * fields of this class with the data thereby obtained.
446
     */
447
    protected function parseDocBlock()
448
    {
449
        if (!$this->docBlockIsParsed) {
450
            $docblock = $this->reflection->getDocComment();
451
            $parser = new CommandDocBlockParser($this);
452
            $parser->parse($docblock);
453
            $this->docBlockIsParsed = true;
454
        }
455
    }
456
457
    /**
458
     * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
459
     * convert the data into the last of these forms.
460
     */
461
    protected static function convertListToCommaSeparated($text)
462
    {
463
        return preg_replace('#[ \t\n\r,]+#', ',', $text);
464
    }
465
}
466