Completed
Pull Request — master (#16)
by Greg
02:24
created

CommandInfo::isAssoc()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
1
<?php
2
namespace Consolidation\AnnotatedCommand;
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
     * Create a new CommandInfo class for a particular method of a class.
69
     *
70
     * @param string|mixed $classNameOrInstance The name of a class, or an
71
     *   instance of it.
72
     * @param string $methodName The name of the method to get info about.
73
     */
74
    public function __construct($classNameOrInstance, $methodName)
75
    {
76
        $this->reflection = new \ReflectionMethod($classNameOrInstance, $methodName);
77
        $this->methodName = $methodName;
78
        // Set up a default name for the command from the method name.
79
        // This can be overridden via @command or @name annotations.
80
        $this->name = $this->convertName($this->reflection->name);
81
        $this->options = new DefaultsWithDescriptions($this->determineOptionsFromParameters(), false);
82
        $this->arguments = new DefaultsWithDescriptions($this->determineAgumentClassifications());
83
    }
84
85
    /**
86
     * Recover the method name provided to the constructor.
87
     *
88
     * @return string
89
     */
90
    public function getMethodName()
91
    {
92
        return $this->methodName;
93
    }
94
95
    /**
96
     * Return the list of refleaction parameters.
97
     *
98
     * @return ReflectionParameter[]
99
     */
100
    public function getParameters()
101
    {
102
        return $this->reflection->getParameters();
103
    }
104
105
    /**
106
     * Get the synopsis of the command (~first line).
107
     *
108
     * @return string
109
     */
110
    public function getDescription()
111
    {
112
        $this->parseDocBlock();
113
        return $this->description;
114
    }
115
116
    /**
117
     * Set the command description.
118
     *
119
     * @param string $description The description to set.
120
     */
121
    public function setDescription($description)
122
    {
123
        $this->description = $description;
124
    }
125
126
    /**
127
     * Get the help text of the command (the description)
128
     */
129
    public function getHelp()
130
    {
131
        $this->parseDocBlock();
132
        return $this->help;
133
    }
134
    /**
135
     * Set the help text for this command.
136
     *
137
     * @param string $help The help text.
138
     */
139
    public function setHelp($help)
140
    {
141
        $this->help = $help;
142
    }
143
144
    /**
145
     * Return the list of aliases for this command.
146
     * @return string[]
147
     */
148
    public function getAliases()
149
    {
150
        $this->parseDocBlock();
151
        return $this->aliases;
152
    }
153
154
    /**
155
     * Set aliases that can be used in place of the command's primary name.
156
     *
157
     * @param string|string[] $aliases
158
     */
159
    public function setAliases($aliases)
160
    {
161
        if (is_string($aliases)) {
162
            $aliases = explode(',', static::convertListToCommaSeparated($aliases));
163
        }
164
        $this->aliases = array_filter($aliases);
165
    }
166
167
    /**
168
     * Return the examples for this command. This is @usage instead of
169
     * @example because the later is defined by the phpdoc standard to
170
     * be example method calls.
171
     *
172
     * @return string[]
173
     */
174
    public function getExampleUsages()
175
    {
176
        $this->parseDocBlock();
177
        return $this->exampleUsage;
178
    }
179
180
    /**
181
     * Return the primary name for this command.
182
     *
183
     * @return string
184
     */
185
    public function getName()
186
    {
187
        $this->parseDocBlock();
188
        return $this->name;
189
    }
190
191
    /**
192
     * Set the primary name for this command.
193
     *
194
     * @param string $name
195
     */
196
    public function setName($name)
197
    {
198
        $this->name = $name;
199
    }
200
201
    /**
202
     * Examine the parameters of the method for this command, and
203
     * build a list of commandline arguements for them.
204
     *
205
     * @return array
206
     */
207
    protected function determineAgumentClassifications()
208
    {
209
        $args = [];
210
        $params = $this->reflection->getParameters();
211
        if (!empty($this->determineOptionsFromParameters())) {
212
            array_pop($params);
213
        }
214
        foreach ($params as $param) {
215
            $defaultValue = $this->getArgumentClassification($param);
216
            if ($defaultValue !== false) {
217
                $args[$param->name] = $defaultValue;
218
            }
219
        }
220
        return $args;
221
    }
222
223
    /**
224
     * Return the commandline arguments for this command. The key
225
     * contains the name of the argument, and the value contains its
226
     * default value. Required commands have a 'null' value.
227
     *
228
     * @return array
229
     */
230
    public function getArguments()
231
    {
232
        return $this->arguments->getValues();
233
    }
234
235
    /**
236
     * Check to see if an argument with the specified name exits.
237
     *
238
     * @param string $name Argument to test for.
239
     * @return boolean
240
     */
241
    public function hasArgument($name)
242
    {
243
        return $this->arguments->exists($name);
0 ignored issues
show
Documentation introduced by
$name is of type string, but the function expects a object<Consolidation\AnnotatedCommand\type>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
244
    }
245
246
    /**
247
     * Set the default value for an argument. A default value of 'null'
248
     * indicates that the argument is required.
249
     *
250
     * @param string $name Name of argument to modify.
251
     * @param string $defaultValue New default value for that argument.
252
     */
253
    public function setArgumentDefaultValue($name, $defaultValue)
254
    {
255
        $this->arguments->setDefaultValue($name, $defaultValue);
256
    }
257
258
    /**
259
     * Add another argument to this command.
260
     *
261
     * @param string $name Name of the argument.
262
     * @param string $description Help text for the argument.
263
     * @param string $defaultValue The default value for the argument.
264
     */
265
    public function addArgument($name, $description, $defaultValue = null)
266
    {
267
        $this->arguments->add($name, $description, $defaultValue);
268
    }
269
270
    /**
271
     * Examine the provided parameter, and determine whether it
272
     * is a parameter that will be filled in with a positional
273
     * commandline argument.
274
     *
275
     * @return false|null|string|array
276
     */
277
    protected function getArgumentClassification($param)
278
    {
279
        $defaultValue = null;
280
        if ($param->isDefaultValueAvailable()) {
281
            $defaultValue = $param->getDefaultValue();
282
            if ($this->isAssoc($defaultValue)) {
283
                return false;
284
            }
285
        }
286
        if ($param->isArray()) {
287
            return [];
288
        }
289
        // Commandline arguments must be strings, so ignore
290
        // any parameter that is typehinted to anything else.
291
        if (($param->getClass() != null) && ($param->getClass() != 'string')) {
292
            return false;
293
        }
294
        return $defaultValue;
295
    }
296
297
    /**
298
     * Return the options for is command. The key is the options name,
299
     * and the value is its default value.
300
     *
301
     * @return array
302
     */
303
    public function getOptions()
304
    {
305
        return $this->options->getValues();
306
    }
307
308
    /**
309
     * Check to see if the specified option exists.
310
     *
311
     * @param string $name Name of the option to check.
312
     * @return boolean
313
     */
314
    public function hasOption($name)
315
    {
316
        return $this->options->exists($name);
0 ignored issues
show
Documentation introduced by
$name is of type string, but the function expects a object<Consolidation\AnnotatedCommand\type>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
317
    }
318
319
    /**
320
     * Change the default value for an option.
321
     *
322
     * @param string $name Option name.
323
     * @param string $defaultValue Option default value.
324
     */
325
    public function setOptionDefaultValue($name, $defaultValue)
326
    {
327
        $this->options->setDefaultValue($name, $defaultValue);
328
    }
329
330
    /**
331
     * Add another option to this command.
332
     *
333
     * @param string $name Option name.
334
     * @param string $description Option description.
335
     * @param string $defaultValue Option default value.
336
     */
337
    public function addOption($name, $description, $defaultValue = null)
338
    {
339
        $this->options->add($name, $description, $defaultValue);
340
    }
341
342
    /**
343
     * Examine the parameters of the method for this command, and determine
344
     * the disposition of the options from them.
345
     *
346
     * @return array
347
     */
348
    public function determineOptionsFromParameters()
349
    {
350
        $params = $this->reflection->getParameters();
351
        if (empty($params)) {
352
            return [];
353
        }
354
        $param = end($params);
355
        if (!$param->isDefaultValueAvailable()) {
356
            return [];
357
        }
358
        if (!$this->isAssoc($param->getDefaultValue())) {
359
            return [];
360
        }
361
        return $param->getDefaultValue();
362
    }
363
364
    /**
365
     * Get the description of one argument.
366
     *
367
     * @param string $name The name of the argument.
368
     * @return string
369
     */
370
    public function getArgumentDescription($name)
371
    {
372
        $this->parseDocBlock();
373
        return $this->arguments->getDescription($name);
374
    }
375
376
    /**
377
     * Get the description of one argument.
378
     *
379
     * @param string $name The name of the option.
380
     * @return string
381
     */
382
    public function getOptionDescription($name)
383
    {
384
        $this->parseDocBlock();
385
        return $this->options->getDescription($name);
386
    }
387
388
    /**
389
     * Helper; determine if an array is associative or not. An array
390
     * is not associative if its keys are numeric, and numbered sequentially
391
     * from zero. All other arrays are considered to be associative.
392
     *
393
     * @param arrau $arr The array
394
     * @return boolean
395
     */
396
    protected function isAssoc($arr)
397
    {
398
        if (!is_array($arr)) {
399
            return false;
400
        }
401
        return array_keys($arr) !== range(0, count($arr) - 1);
402
    }
403
404
    /**
405
     * Get any annotations included in the docblock comment for the
406
     * implementation method of this command that are not already
407
     * handled by the primary methods of this class.
408
     *
409
     * @return array
410
     */
411
    public function getAnnotations()
412
    {
413
        $this->parseDocBlock();
414
        return $this->otherAnnotations;
415
    }
416
417
    /**
418
     * Return a specific named annotation for this command.
419
     *
420
     * @param string $annotation The name of the annotation.
421
     * @return string
422
     */
423
    public function getAnnotation($annotation)
424
    {
425
        // hasAnnotation parses the docblock
426
        if (!$this->hasAnnotation($annotation)) {
427
            return null;
428
        }
429
        return $this->otherAnnotations[$annotation];
430
    }
431
432
    /**
433
     * Check to see if the specified annotation exists for this command.
434
     *
435
     * @param string $annotation The name of the annotation.
436
     * @return boolean
437
     */
438
    public function hasAnnotation($annotation)
439
    {
440
        $this->parseDocBlock();
441
        return array_key_exists($annotation, $this->otherAnnotations);
442
    }
443
444
    /**
445
     * Convert from a method name to the corresponding command name. A
446
     * method 'fooBar' will become 'foo:bar', and 'fooBarBazBoz' will
447
     * become 'foo:bar-baz-boz'.
448
     *
449
     * @param type $camel method name.
450
     * @return string
451
     */
452
    protected function convertName($camel)
453
    {
454
        $splitter="-";
455
        $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel));
456
        $camel = preg_replace("/$splitter/", ':', $camel, 1);
457
        return strtolower($camel);
458
    }
459
460
    /**
461
     * Add an example usage for this command.
462
     *
463
     * @param string $usage An example of the command, including the command
464
     *   name and all of its example arguments and options.
465
     * @param string $description An explanation of what the example does.
466
     */
467
    public function setExampleUsage($usage, $description)
468
    {
469
        $this->exampleUsage[$usage] = $description;
470
    }
471
472
    /**
473
     * Parse the docBlock comment for this command, and set the
474
     * fields of this class with the data thereby obtained.
475
     */
476
    protected function parseDocBlock()
477
    {
478
        if (!$this->docBlockIsParsed) {
479
            $docblock = $this->reflection->getDocComment();
480
            $parser = new CommandDocBlockParser($this);
481
            $parser->parse($docblock);
482
            $this->docBlockIsParsed = true;
483
        }
484
    }
485
486
    /**
487
     * Save any tag that we do not explicitly recognize in the
488
     * 'otherAnnotations' map.
489
     */
490
    public function addOtherAnnotation($name, $content)
491
    {
492
        $this->otherAnnotations[$name] = $content;
493
    }
494
495
    /**
496
     * An option might have a name such as 'silent|s'. In this
497
     * instance, we will allow the @option or @default tag to
498
     * reference the option only by name (e.g. 'silent' or 's'
499
     * instead of 'silent|s').
500
     */
501
    public function findMatchingOption($optionName)
502
    {
503
        // Exit fast if there's an exact match
504
        if ($this->options->exists($optionName)) {
505
            return $optionName;
506
        }
507
        // Check to see if we can find the option name in an existing option,
508
        // e.g. if the options array has 'silent|s' => false, and the annotation
509
        // is @silent.
510
        foreach ($this->options->getValues() as $name => $default) {
511
            if (in_array($optionName, explode('|', $name))) {
512
                return $name;
513
            }
514
        }
515
        // Check the other direction: if the annotation contains @silent|s
516
        // and the options array has 'silent|s'.
517
        $checkMatching = explode('|', $optionName);
518
        if (count($checkMatching) > 1) {
519
            foreach ($checkMatching as $checkName) {
520
                if ($this->options->exists($checkName)) {
521
                    $this->options->rename($checkName, $optionName);
522
                    return $optionName;
523
                }
524
            }
525
        }
526
        return $optionName;
527
    }
528
529
    /**
530
     * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
531
     * convert the data into the last of these forms.
532
     */
533
    protected static function convertListToCommaSeparated($text)
534
    {
535
        return preg_replace('#[ \t\n\r,]+#', ',', $text);
536
    }
537
}
538