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

CommandInfo::getAnnotationsForCommand()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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