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

CommandInfo::getHelp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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