Completed
Pull Request — master (#65)
by
unknown
04:30
created

CommandInfo::getParameters()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
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
     * @var array
88
     */
89
    protected $calls = [];
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 View Code Duplication
    public function setAliases($aliases)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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
        if (!$this->hasAnnotation('topics')) {
335
            return [];
336
        }
337
        $topics = $this->getAnnotation('topics');
338
        return explode(',', trim($topics));
339
    }
340
341
    /**
342
     * Returns an array of commands that this command calls.
343
     *
344
     * @return string[]
345
     */
346
    public function getCalls()
347
    {
348
        return $this->calls;
349
    }
350
351 View Code Duplication
    public function setCalls($calls) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
352
        if (is_string($calls)) {
353
            $calls = explode(',', static::convertListToCommaSeparated($calls));
354
        }
355
        $this->calls = array_filter($calls);
356
357
        return $this;
358
    }
359
360
    /**
361
     * Return the list of refleaction parameters.
362
     *
363
     * @return ReflectionParameter[]
364
     */
365
    public function getParameters()
366
    {
367
        return $this->reflection->getParameters();
368
    }
369
370
    /**
371
     * Descriptions of commandline arguements for this command.
372
     *
373
     * @return DefaultsWithDescriptions
374
     */
375
    public function arguments()
376
    {
377
        return $this->arguments;
378
    }
379
380
    /**
381
     * Descriptions of commandline options for this command.
382
     *
383
     * @return DefaultsWithDescriptions
384
     */
385
    public function options()
386
    {
387
        return $this->options;
388
    }
389
390
    /**
391
     * Return the name of the last parameter if it holds the options.
392
     */
393
    public function optionParamName()
394
    {
395
        return $this->optionParamName;
396
    }
397
398
    /**
399
     * Get the inputOptions for the options associated with this CommandInfo
400
     * object, e.g. via @option annotations, or from
401
     * $options = ['someoption' => 'defaultvalue'] in the command method
402
     * parameter list.
403
     *
404
     * @return InputOption[]
405
     */
406
    public function inputOptions()
407
    {
408
        $explicitOptions = [];
409
410
        $opts = $this->options()->getValues();
411
        foreach ($opts as $name => $defaultValue) {
412
            $description = $this->options()->getDescription($name);
413
414
            $fullName = $name;
415
            $shortcut = '';
416
            if (strpos($name, '|')) {
417
                list($fullName, $shortcut) = explode('|', $name, 2);
418
            }
419
420
            if (is_bool($defaultValue)) {
421
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_NONE, $description);
422
            } elseif ($defaultValue === InputOption::VALUE_REQUIRED) {
423
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_REQUIRED, $description);
424
            } else {
425
                $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $defaultValue);
426
            }
427
        }
428
429
        return $explicitOptions;
430
    }
431
432
    /**
433
     * An option might have a name such as 'silent|s'. In this
434
     * instance, we will allow the @option or @default tag to
435
     * reference the option only by name (e.g. 'silent' or 's'
436
     * instead of 'silent|s').
437
     *
438
     * @param string $optionName
439
     * @return string
440
     */
441
    public function findMatchingOption($optionName)
442
    {
443
        // Exit fast if there's an exact match
444
        if ($this->options->exists($optionName)) {
445
            return $optionName;
446
        }
447
        $existingOptionName = $this->findExistingOption($optionName);
448
        if (isset($existingOptionName)) {
449
            return $existingOptionName;
450
        }
451
        return $this->findOptionAmongAlternatives($optionName);
452
    }
453
454
    /**
455
     * @param string $optionName
456
     * @return string
457
     */
458
    protected function findOptionAmongAlternatives($optionName)
459
    {
460
        // Check the other direction: if the annotation contains @silent|s
461
        // and the options array has 'silent|s'.
462
        $checkMatching = explode('|', $optionName);
463
        if (count($checkMatching) > 1) {
464
            foreach ($checkMatching as $checkName) {
465
                if ($this->options->exists($checkName)) {
466
                    $this->options->rename($checkName, $optionName);
467
                    return $optionName;
468
                }
469
            }
470
        }
471
        return $optionName;
472
    }
473
474
    /**
475
     * @param string $optionName
476
     * @return string|null
477
     */
478
    protected function findExistingOption($optionName)
479
    {
480
        // Check to see if we can find the option name in an existing option,
481
        // e.g. if the options array has 'silent|s' => false, and the annotation
482
        // is @silent.
483
        foreach ($this->options()->getValues() as $name => $default) {
484
            if (in_array($optionName, explode('|', $name))) {
485
                return $name;
486
            }
487
        }
488
    }
489
490
    /**
491
     * Examine the parameters of the method for this command, and
492
     * build a list of commandline arguements for them.
493
     *
494
     * @return array
495
     */
496
    protected function determineAgumentClassifications()
497
    {
498
        $result = new DefaultsWithDescriptions();
499
        $params = $this->reflection->getParameters();
500
        $optionsFromParameters = $this->determineOptionsFromParameters();
501
        if (!empty($optionsFromParameters)) {
502
            array_pop($params);
503
        }
504
        foreach ($params as $param) {
505
            $this->addParameterToResult($result, $param);
506
        }
507
        return $result;
508
    }
509
510
    /**
511
     * Examine the provided parameter, and determine whether it
512
     * is a parameter that will be filled in with a positional
513
     * commandline argument.
514
     */
515
    protected function addParameterToResult($result, $param)
516
    {
517
        // Commandline arguments must be strings, so ignore any
518
        // parameter that is typehinted to any non-primative class.
519
        if ($param->getClass() != null) {
520
            return;
521
        }
522
        $result->add($param->name);
523
        if ($param->isDefaultValueAvailable()) {
524
            $defaultValue = $param->getDefaultValue();
525
            if (!$this->isAssoc($defaultValue)) {
526
                $result->setDefaultValue($param->name, $defaultValue);
527
            }
528
        } elseif ($param->isArray()) {
529
            $result->setDefaultValue($param->name, []);
530
        }
531
    }
532
533
    /**
534
     * Examine the parameters of the method for this command, and determine
535
     * the disposition of the options from them.
536
     *
537
     * @return array
538
     */
539
    protected function determineOptionsFromParameters()
540
    {
541
        $params = $this->reflection->getParameters();
542
        if (empty($params)) {
543
            return [];
544
        }
545
        $param = end($params);
546
        if (!$param->isDefaultValueAvailable()) {
547
            return [];
548
        }
549
        if (!$this->isAssoc($param->getDefaultValue())) {
550
            return [];
551
        }
552
        return $param->getDefaultValue();
553
    }
554
555
    protected function lastParameterName()
556
    {
557
        $params = $this->reflection->getParameters();
558
        $param = end($params);
559
        if (!$param) {
560
            return '';
561
        }
562
        return $param->name;
563
    }
564
565
    /**
566
     * Helper; determine if an array is associative or not. An array
567
     * is not associative if its keys are numeric, and numbered sequentially
568
     * from zero. All other arrays are considered to be associative.
569
     *
570
     * @param arrau $arr The array
571
     * @return boolean
572
     */
573
    protected function isAssoc($arr)
574
    {
575
        if (!is_array($arr)) {
576
            return false;
577
        }
578
        return array_keys($arr) !== range(0, count($arr) - 1);
579
    }
580
581
    /**
582
     * Convert from a method name to the corresponding command name. A
583
     * method 'fooBar' will become 'foo:bar', and 'fooBarBazBoz' will
584
     * become 'foo:bar-baz-boz'.
585
     *
586
     * @param string $camel method name.
587
     * @return string
588
     */
589
    protected function convertName($camel)
590
    {
591
        $splitter="-";
592
        $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel));
593
        $camel = preg_replace("/$splitter/", ':', $camel, 1);
594
        return strtolower($camel);
595
    }
596
597
    /**
598
     * Parse the docBlock comment for this command, and set the
599
     * fields of this class with the data thereby obtained.
600
     */
601
    protected function parseDocBlock()
602
    {
603
        if (!$this->docBlockIsParsed) {
604
            // The parse function will insert data from the provided method
605
            // into this object, using our accessors.
606
            CommandDocBlockParserFactory::parse($this, $this->reflection);
607
            $this->docBlockIsParsed = true;
608
        }
609
    }
610
611
    /**
612
     * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
613
     * convert the data into the last of these forms.
614
     */
615
    protected static function convertListToCommaSeparated($text)
616
    {
617
        return preg_replace('#[ \t\n\r,]+#', ',', $text);
618
    }
619
}
620