Completed
Push — master ( 26196b...46a28a )
by Greg
02:25
created

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