Completed
Push — master ( 2066be...cd3bc8 )
by Greg
02:15
created

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