Completed
Push — master ( f05b3a...598904 )
by Greg
02:21
created

CommandInfo::getAnnotations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
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
        // Check to see if we can find the option name in an existing option,
260
        // e.g. if the options array has 'silent|s' => false, and the annotation
261
        // is @silent.
262
        foreach ($this->options as $name => $default) {
263
            if (in_array($optionName, explode('|', $name))) {
264
                return $name;
265
            }
266
        }
267
        // Check the other direction: if the annotation contains @silent|s
268
        // and the options array has 'silent|s'.
269
        $checkMatching = explode('|', $optionName);
270
        if (count($checkMatching) > 1) {
271
            foreach ($checkMatching as $checkName) {
272
                if (isset($this->options[$checkName])) {
273
                    $this->options[$optionName] = $this->options[$checkName];
274
                    unset($this->options[$checkName]);
275
                    return $optionName;
276
                }
277
            }
278
        }
279
        return $optionName;
280
    }
281
282
    protected function initializeFromParameters($params)
283
    {
284
        // Set up a default name for the command from the method name.
285
        // This can be overridden via @command or @name annotations.
286
        $this->params = $params;
287
        $this->options = $this->determineOptionsFromParameters($this->params);
288
        $this->arguments = $this->determineAgumentClassifications($this->params);
289
    }
290
291
    /**
292
     * Parse the docBlock comment for this command, and set the
293
     * fields of this class with the data thereby obtained.
294
     */
295
    protected function parseDocBlock($docblock)
296
    {
297
        $parser = new CommandDocBlockParser($this);
298
        $parser->parse($docblock);
299
    }
300
301
    protected function determineAgumentClassifications($params)
302
    {
303
        $args = [];
304
        if (!empty($this->determineOptionsFromParameters($params))) {
305
            array_pop($params);
306
        }
307
        foreach ($params as $param) {
308
            $defaultValue = $this->getArgumentClassification($param);
309
            if ($defaultValue !== false) {
310
                $args[$param->name] = $defaultValue;
311
            }
312
        }
313
        return $args;
314
    }
315
316
    protected function determineOptionsFromParameters($params)
317
    {
318
        if (empty($params)) {
319
            return [];
320
        }
321
        $param = end($params);
322
        if (!$param->isDefaultValueAvailable()) {
323
            return [];
324
        }
325
        if (!$this->isAssoc($param->getDefaultValue())) {
326
            return [];
327
        }
328
        return $param->getDefaultValue();
329
    }
330
331
    /**
332
     * Examine the provided parameter, and determine whether it
333
     * is a parameter that will be filled in with a positional
334
     * commandline argument.
335
     *
336
     * @return false|null|string|array
337
     */
338
    protected function getArgumentClassification($param)
339
    {
340
        $defaultValue = null;
341
        if ($param->isDefaultValueAvailable()) {
342
            $defaultValue = $param->getDefaultValue();
343
            if ($this->isAssoc($defaultValue)) {
344
                return false;
345
            }
346
        }
347
        if ($param->isArray()) {
348
            return [];
349
        }
350
        // Commandline arguments must be strings, so ignore
351
        // any parameter that is typehinted to anything else.
352
        if (($param->getClass() != null) && ($param->getClass() != 'string')) {
353
            return false;
354
        }
355
        return $defaultValue;
356
    }
357
358
    protected function setDefaultName($methodName)
359
    {
360
        $this->name = $this->convertName($methodName);
361
    }
362
363
    protected function convertName($camel)
364
    {
365
        $splitter="-";
366
        $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel));
367
        $camel = preg_replace("/$splitter/", ':', $camel, 1);
368
        return strtolower($camel);
369
    }
370
371
    protected function isAssoc($arr)
372
    {
373
        if (!is_array($arr)) {
374
            return false;
375
        }
376
        return array_keys($arr) !== range(0, count($arr) - 1);
377
    }
378
379
    /**
380
     * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c',
381
     * convert the data into the last of these forms.
382
     */
383
    protected static function convertListToCommaSeparated($text)
384
    {
385
        return preg_replace('#[ \t\n\r,]+#', ',', $text);
386
    }
387
}
388