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

CommandInfo::getAnnotations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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