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

CommandInfo::parseDocBlock()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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