Completed
Push — master ( 598904...0763ed )
by Greg
02:24
created

CommandInfo::getDescription()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 0
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 \ReflectionParameter
13
     */
14
    protected $params;
15
16
    /**
17
     * @var string
18
     */
19
    protected $name;
20
21
    /**
22
     * @var string
23
     */
24
    protected $description = '';
25
26
    /**
27
     * @var string
28
     */
29
    protected $help = '';
30
31
    /**
32
     * @var array
33
     */
34
    protected $options = [];
35
36
    /**
37
     * @var array
38
     */
39
    protected $arguments = [];
40
41
    /**
42
     * @var array
43
     */
44
    protected $argumentDescriptions = [];
45
46
    /**
47
     * @var array
48
     */
49
    protected $optionDescriptions = [];
50
51
    /**
52
     * @var array
53
     */
54
    protected $exampleUsage = [];
55
56
    /**
57
     * @var array
58
     */
59
    protected $otherAnnotations = [];
60
61
    /**
62
     * @var array
63
     */
64
    protected $aliases = [];
65
66
    /**
67
     * @var string
68
     */
69
    protected $methodName;
70
71
    public function __construct($classNameOrInstance, $methodName)
72
    {
73
        $reflectionMethod = new \ReflectionMethod($classNameOrInstance, $methodName);
74
        $this->methodName = $methodName;
75
        $this->setDefaultName($methodName);
76
        $this->initializeFromParameters($reflectionMethod->getParameters());
77
        $this->parseDocBlock($reflectionMethod->getDocComment());
78
    }
79
80
    public function getMethodName()
81
    {
82
        return $this->methodName;
83
    }
84
85
    public function getParameters()
86
    {
87
        return $this->params;
88
    }
89
90
    /**
91
     * Get the synopsis of the command (~first line).
92
     */
93
    public function getDescription()
94
    {
95
        return $this->description;
96
    }
97
98
    public function setDescription($description)
99
    {
100
        $this->description = $description;
101
    }
102
103
    /**
104
     * Get the help text of the command (the description)
105
     */
106
    public function getHelp()
107
    {
108
        return $this->help;
109
    }
110
111
    public function setHelp($help)
112
    {
113
        $this->help = $help;
114
    }
115
116
    public function getAliases()
117
    {
118
        return $this->aliases;
119
    }
120
121
    public function setAliases($aliases)
122
    {
123
        if (is_string($aliases)) {
124
            $aliases = explode(',', static::convertListToCommaSeparated($aliases));
125
        }
126
        $this->aliases = array_filter($aliases);
127
    }
128
129
    public function getExampleUsages()
130
    {
131
        return $this->exampleUsage;
132
    }
133
134
    public function getName()
135
    {
136
        return $this->name;
137
    }
138
139
    public function setName($name)
140
    {
141
        $this->name = $name;
142
    }
143
144
    public function getArguments()
145
    {
146
        return $this->arguments;
147
    }
148
149
    public function hasArgument($name)
150
    {
151
        return array_key_exists($name, $this->arguments);
152
    }
153
154
    public function setArgumentDefaultValue($name, $defaultValue)
155
    {
156
        $this->arguments[$name] = $defaultValue;
157
    }
158
159
    public function addArgument($name, $description, $defaultValue = null)
160
    {
161
        if (!$this->hasArgument($name) || isset($defaultValue)) {
162
            $this->arguments[$name] = $defaultValue;
163
        }
164
        unset($this->argumentDescriptions[$name]);
165
        if (isset($description)) {
166
            $this->argumentDescriptions[$name] = $description;
167
        }
168
    }
169
170
    public function getOptions()
171
    {
172
        return $this->options;
173
    }
174
175
    public function hasOption($name)
176
    {
177
        return array_key_exists($name, $this->options);
178
    }
179
180
    public function setOptionDefaultValue($name, $defaultValue)
181
    {
182
        $this->options[$name] = $defaultValue;
183
    }
184
185
    public function addOption($name, $description, $defaultValue = false)
186
    {
187
        if (!$this->hasOption($name) || $defaultValue) {
188
            $this->options[$name] = $defaultValue;
189
        }
190
        unset($this->optionDescriptions[$name]);
191
        if (isset($description)) {
192
            $this->optionDescriptions[$name] = $description;
193
        }
194
    }
195
196
    public function getArgumentDescription($name)
197
    {
198
        if (array_key_exists($name, $this->argumentDescriptions)) {
199
            return $this->argumentDescriptions[$name];
200
        }
201
202
        return '';
203
    }
204
205
    public function getOptionDescription($name)
206
    {
207
        if (array_key_exists($name, $this->optionDescriptions)) {
208
            return $this->optionDescriptions[$name];
209
        }
210
211
        return '';
212
    }
213
214
    public function getAnnotations()
215
    {
216
        return $this->otherAnnotations;
217
    }
218
219
    public function getAnnotation($annotation)
220
    {
221
        // hasAnnotation parses the docblock
222
        if (!$this->hasAnnotation($annotation)) {
223
            return null;
224
        }
225
        return $this->otherAnnotations[$annotation];
226
    }
227
228
    public function hasAnnotation($annotation)
229
    {
230
        return array_key_exists($annotation, $this->otherAnnotations);
231
    }
232
233
    public function setExampleUsage($usage, $description)
234
    {
235
        $this->exampleUsage[$usage] = $description;
236
    }
237
238
    /**
239
     * Save any tag that we do not explicitly recognize in the
240
     * 'otherAnnotations' map.
241
     */
242
    public function addOtherAnnotation($name, $content)
243
    {
244
        $this->otherAnnotations[$name] = $content;
245
    }
246
247
    /**
248
     * An option might have a name such as 'silent|s'. In this
249
     * instance, we will allow the @option or @default tag to
250
     * reference the option only by name (e.g. 'silent' or 's'
251
     * instead of 'silent|s').
252
     */
253
    public function findMatchingOption($optionName)
254
    {
255
        // Exit fast if there's an exact match
256
        if (isset($this->options[$optionName])) {
257
            return $optionName;
258
        }
259
        $existingOptionName = $this->findExistingOption($optionName);
260
        if (isset($existingOptionName)) {
261
            return $existingOptionName;
262
        }
263
        return $this->findOptionAmongAlternatives($optionName);
264
    }
265
266
    protected function findOptionAmongAlternatives($optionName)
267
    {
268
        // Check the other direction: if the annotation contains @silent|s
269
        // and the options array has 'silent|s'.
270
        $checkMatching = explode('|', $optionName);
271
        if (count($checkMatching) > 1) {
272
            foreach ($checkMatching as $checkName) {
273
                if (isset($this->options[$checkName])) {
274
                    $this->options[$optionName] = $this->options[$checkName];
275
                    unset($this->options[$checkName]);
276
                    return $optionName;
277
                }
278
            }
279
        }
280
        return $optionName;
281
    }
282
283
    protected function findExistingOption($optionName)
284
    {
285
        // Check to see if we can find the option name in an existing option,
286
        // e.g. if the options array has 'silent|s' => false, and the annotation
287
        // is @silent.
288
        foreach ($this->options as $name => $default) {
289
            if (in_array($optionName, explode('|', $name))) {
290
                return $name;
291
            }
292
        }
293
    }
294
295
    protected function initializeFromParameters($params)
296
    {
297
        // Set up a default name for the command from the method name.
298
        // This can be overridden via @command or @name annotations.
299
        $this->params = $params;
300
        $this->options = $this->determineOptionsFromParameters($this->params);
301
        $this->arguments = $this->determineAgumentClassifications($this->params);
302
    }
303
304
    /**
305
     * Parse the docBlock comment for this command, and set the
306
     * fields of this class with the data thereby obtained.
307
     */
308
    protected function parseDocBlock($docblock)
309
    {
310
        $parser = new CommandDocBlockParser($this);
311
        $parser->parse($docblock);
312
    }
313
314
    protected function determineAgumentClassifications($params)
315
    {
316
        $args = [];
317
        if (!empty($this->determineOptionsFromParameters($params))) {
318
            array_pop($params);
319
        }
320
        foreach ($params as $param) {
321
            $defaultValue = $this->getArgumentClassification($param);
322
            if ($defaultValue !== false) {
323
                $args[$param->name] = $defaultValue;
324
            }
325
        }
326
        return $args;
327
    }
328
329
    protected function determineOptionsFromParameters($params)
330
    {
331
        if (empty($params)) {
332
            return [];
333
        }
334
        $param = end($params);
335
        if (!$param->isDefaultValueAvailable()) {
336
            return [];
337
        }
338
        if (!$this->isAssoc($param->getDefaultValue())) {
339
            return [];
340
        }
341
        return $param->getDefaultValue();
342
    }
343
344
    /**
345
     * Examine the provided parameter, and determine whether it
346
     * is a parameter that will be filled in with a positional
347
     * commandline argument.
348
     *
349
     * @return false|null|string|array
350
     */
351
    protected function getArgumentClassification($param)
352
    {
353
        $defaultValue = null;
354
        if ($param->isDefaultValueAvailable()) {
355
            $defaultValue = $param->getDefaultValue();
356
            if ($this->isAssoc($defaultValue)) {
357
                return false;
358
            }
359
        }
360
        if ($param->isArray()) {
361
            return [];
362
        }
363
        // Commandline arguments must be strings, so ignore
364
        // any parameter that is typehinted to anything else.
365
        if (($param->getClass() != null) && ($param->getClass() != 'string')) {
366
            return false;
367
        }
368
        return $defaultValue;
369
    }
370
371
    protected function setDefaultName($methodName)
372
    {
373
        $this->name = $this->convertName($methodName);
374
    }
375
376
    protected function convertName($camel)
377
    {
378
        $splitter="-";
379
        $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel));
380
        $camel = preg_replace("/$splitter/", ':', $camel, 1);
381
        return strtolower($camel);
382
    }
383
384
    protected function isAssoc($arr)
385
    {
386
        if (!is_array($arr)) {
387
            return false;
388
        }
389
        return array_keys($arr) !== range(0, count($arr) - 1);
390
    }
391
392
    /**
393
     * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
394
     * convert the data into the last of these forms.
395
     */
396
    protected static function convertListToCommaSeparated($text)
397
    {
398
        return preg_replace('#[ \t\n\r,]+#', ',', $text);
399
    }
400
}
401