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
|
|
|
* Serialization schema version. Incremented every time the serialization schema changes. |
22
|
|
|
*/ |
23
|
|
|
const SERIALIZATION_SCHEMA_VERSION = 1; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* @var \ReflectionMethod |
27
|
|
|
*/ |
28
|
|
|
protected $reflection; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @var boolean |
32
|
|
|
* @var string |
33
|
|
|
*/ |
34
|
|
|
protected $docBlockIsParsed = false; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var string |
38
|
|
|
*/ |
39
|
|
|
protected $name; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var string |
43
|
|
|
*/ |
44
|
|
|
protected $description = ''; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @var string |
48
|
|
|
*/ |
49
|
|
|
protected $help = ''; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var DefaultsWithDescriptions |
53
|
|
|
*/ |
54
|
|
|
protected $options; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var DefaultsWithDescriptions |
58
|
|
|
*/ |
59
|
|
|
protected $arguments; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @var array |
63
|
|
|
*/ |
64
|
|
|
protected $exampleUsage = []; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @var AnnotationData |
68
|
|
|
*/ |
69
|
|
|
protected $otherAnnotations; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @var array |
73
|
|
|
*/ |
74
|
|
|
protected $aliases = []; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @var InputOption[] |
78
|
|
|
*/ |
79
|
|
|
protected $inputOptions; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* @var string |
83
|
|
|
*/ |
84
|
|
|
protected $methodName; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @var string |
88
|
|
|
*/ |
89
|
|
|
protected $returnType; |
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, or an array of cached data. |
96
|
|
|
* @param string $methodName The name of the method to get info about. |
97
|
|
|
* @param array $cache Cached data |
98
|
|
|
* @deprecated Use CommandInfo::create() or CommandInfo::deserialize() |
99
|
|
|
* instead. In the future, this constructor will be protected. |
100
|
|
|
*/ |
101
|
|
|
public function __construct($classNameOrInstance, $methodName, $cache = []) |
102
|
|
|
{ |
103
|
|
|
$this->reflection = new \ReflectionMethod($classNameOrInstance, $methodName); |
104
|
|
|
$this->methodName = $methodName; |
105
|
|
|
|
106
|
|
|
// If the cache came from a newer version, ignore it and |
107
|
|
|
// regenerate the cached information. |
108
|
|
|
if (!empty($cache) && static::isValidSerializedData($cache) && !$this->cachedFileIsModified($cache)) { |
109
|
|
|
$this->constructFromCache($cache); |
110
|
|
|
$this->docBlockIsParsed = true; |
111
|
|
|
} else { |
112
|
|
|
$this->constructFromClassAndMethod($classNameOrInstance, $methodName); |
113
|
|
|
} |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
public static function create($classNameOrInstance, $methodName) |
117
|
|
|
{ |
118
|
|
|
return new self($classNameOrInstance, $methodName); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
View Code Duplication |
public static function deserialize($cache) |
|
|
|
|
122
|
|
|
{ |
123
|
|
|
$cache = (array)$cache; |
124
|
|
|
|
125
|
|
|
$className = $cache['class']; |
126
|
|
|
$methodName = $cache['method_name']; |
127
|
|
|
|
128
|
|
|
return new self($className, $methodName, $cache); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
View Code Duplication |
protected static function cachedMethodExists($cache) |
|
|
|
|
132
|
|
|
{ |
133
|
|
|
$cache = (array)$cache; |
134
|
|
|
|
135
|
|
|
$className = $cache['class']; |
136
|
|
|
$methodName = $cache['method_name']; |
137
|
|
|
|
138
|
|
|
return method_exists($className, $methodName); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
public static function isValidSerializedData($cache) |
142
|
|
|
{ |
143
|
|
|
return |
144
|
|
|
isset($cache['schema']) && |
145
|
|
|
isset($cache['method_name']) && |
146
|
|
|
isset($cache['mtime']) && |
147
|
|
|
($cache['schema'] > 0) && |
148
|
|
|
($cache['schema'] <= self::SERIALIZATION_SCHEMA_VERSION) && |
149
|
|
|
self::cachedMethodExists($cache); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
public function cachedFileIsModified($cache) |
153
|
|
|
{ |
154
|
|
|
$path = $this->reflection->getFileName(); |
155
|
|
|
return filemtime($path) != $cache['mtime']; |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
protected function constructFromClassAndMethod($classNameOrInstance, $methodName) |
|
|
|
|
159
|
|
|
{ |
160
|
|
|
$this->otherAnnotations = new AnnotationData(); |
161
|
|
|
// Set up a default name for the command from the method name. |
162
|
|
|
// This can be overridden via @command or @name annotations. |
163
|
|
|
$this->name = $this->convertName($methodName); |
164
|
|
|
$this->options = new DefaultsWithDescriptions($this->determineOptionsFromParameters(), false); |
165
|
|
|
$this->arguments = $this->determineAgumentClassifications(); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
protected function constructFromCache($info_array) |
169
|
|
|
{ |
170
|
|
|
$info_array += $this->defaultSerializationData(); |
171
|
|
|
|
172
|
|
|
$this->name = $info_array['name']; |
173
|
|
|
$this->methodName = $info_array['method_name']; |
174
|
|
|
$this->otherAnnotations = new AnnotationData((array) $info_array['annotations']); |
175
|
|
|
$this->arguments = new DefaultsWithDescriptions(); |
176
|
|
|
$this->options = new DefaultsWithDescriptions(); |
177
|
|
|
$this->aliases = $info_array['aliases']; |
178
|
|
|
$this->help = $info_array['help']; |
179
|
|
|
$this->description = $info_array['description']; |
180
|
|
|
$this->exampleUsage = $info_array['example_usages']; |
181
|
|
|
$this->returnType = $info_array['return_type']; |
|
|
|
|
182
|
|
|
|
183
|
|
View Code Duplication |
foreach ((array)$info_array['arguments'] as $key => $info) { |
|
|
|
|
184
|
|
|
$info = (array)$info; |
185
|
|
|
$this->arguments->add($key, $info['description']); |
186
|
|
|
if (array_key_exists('default', $info)) { |
187
|
|
|
$this->arguments->setDefaultValue($key, $info['default']); |
188
|
|
|
} |
189
|
|
|
} |
190
|
|
View Code Duplication |
foreach ((array)$info_array['options'] as $key => $info) { |
|
|
|
|
191
|
|
|
$info = (array)$info; |
192
|
|
|
$this->options->add($key, $info['description']); |
193
|
|
|
if (array_key_exists('default', $info)) { |
194
|
|
|
$this->options->setDefaultValue($key, $info['default']); |
195
|
|
|
} |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
$this->input_options = []; |
|
|
|
|
199
|
|
|
foreach ((array)$info_array['input_options'] as $i => $option) { |
200
|
|
|
$option = (array) $option; |
201
|
|
|
$this->inputOptions[$i] = new InputOption( |
202
|
|
|
$option['name'], |
203
|
|
|
$option['shortcut'], |
204
|
|
|
$option['mode'], |
205
|
|
|
$option['description'], |
206
|
|
|
$option['default'] |
207
|
|
|
); |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
public function serialize() |
212
|
|
|
{ |
213
|
|
|
$path = $this->reflection->getFileName(); |
214
|
|
|
|
215
|
|
|
$info = [ |
216
|
|
|
'schema' => self::SERIALIZATION_SCHEMA_VERSION, |
217
|
|
|
'class' => $this->reflection->getDeclaringClass()->getName(), |
218
|
|
|
'method_name' => $this->getMethodName(), |
219
|
|
|
'name' => $this->getName(), |
220
|
|
|
'description' => $this->getDescription(), |
221
|
|
|
'help' => $this->getHelp(), |
222
|
|
|
'aliases' => $this->getAliases(), |
223
|
|
|
'annotations' => $this->getAnnotations()->getArrayCopy(), |
224
|
|
|
// Todo: Test This. |
225
|
|
|
'topics' => $this->getTopics(), |
226
|
|
|
'example_usages' => $this->getExampleUsages(), |
227
|
|
|
'return_type' => $this->getReturnType(), |
228
|
|
|
'mtime' => filemtime($path), |
229
|
|
|
] + $this->defaultSerializationData(); |
230
|
|
View Code Duplication |
foreach ($this->arguments()->getValues() as $key => $val) { |
|
|
|
|
231
|
|
|
$info['arguments'][$key] = [ |
232
|
|
|
'description' => $this->arguments()->getDescription($key), |
233
|
|
|
]; |
234
|
|
|
if ($this->arguments()->hasDefault($key)) { |
235
|
|
|
$info['arguments'][$key]['default'] = $val; |
236
|
|
|
} |
237
|
|
|
} |
238
|
|
View Code Duplication |
foreach ($this->options()->getValues() as $key => $val) { |
|
|
|
|
239
|
|
|
$info['options'][$key] = [ |
240
|
|
|
'description' => $this->options()->getDescription($key), |
241
|
|
|
]; |
242
|
|
|
if ($this->options()->hasDefault($key)) { |
243
|
|
|
$info['options'][$key]['default'] = $val; |
244
|
|
|
} |
245
|
|
|
} |
246
|
|
|
foreach ($this->getParameters() as $i => $parameter) { |
|
|
|
|
247
|
|
|
// TODO: Also cache input/output params |
248
|
|
|
} |
249
|
|
|
foreach ($this->inputOptions() as $i => $option) { |
250
|
|
|
$mode = 0; |
251
|
|
|
if ($option->isValueRequired()) { |
252
|
|
|
$mode |= InputOption::VALUE_REQUIRED; |
253
|
|
|
} |
254
|
|
|
if ($option->isValueOptional()) { |
255
|
|
|
$mode |= InputOption::VALUE_OPTIONAL; |
256
|
|
|
} |
257
|
|
|
if ($option->isArray()) { |
258
|
|
|
$mode |= InputOption::VALUE_IS_ARRAY; |
259
|
|
|
} |
260
|
|
|
if (!$mode) { |
261
|
|
|
$mode = InputOption::VALUE_NONE; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
$info['input_options'][$i] = [ |
265
|
|
|
'name' => $option->getName(), |
266
|
|
|
'shortcut' => $option->getShortcut(), |
267
|
|
|
'mode' => $mode, |
268
|
|
|
'description' => $option->getDescription(), |
269
|
|
|
'default' => null, |
270
|
|
|
]; |
271
|
|
|
if ($option->isValueOptional()) { |
272
|
|
|
$info['input_options'][$i]['default'] = $option->getDefault(); |
273
|
|
|
} |
274
|
|
|
} |
275
|
|
|
return $info; |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Default data for serialization. |
280
|
|
|
* @return array |
281
|
|
|
*/ |
282
|
|
|
protected function defaultSerializationData() |
283
|
|
|
{ |
284
|
|
|
return [ |
285
|
|
|
'description' => '', |
286
|
|
|
'help' => '', |
287
|
|
|
'aliases' => [], |
288
|
|
|
'annotations' => [], |
289
|
|
|
'topics' => [], |
290
|
|
|
'example_usages' => [], |
291
|
|
|
'return_type' => [], |
292
|
|
|
'parameters' => [], |
293
|
|
|
'arguments' => [], |
294
|
|
|
'options' => [], |
295
|
|
|
'input_options' => [], |
296
|
|
|
'mtime' => 0, |
297
|
|
|
]; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Recover the method name provided to the constructor. |
302
|
|
|
* |
303
|
|
|
* @return string |
304
|
|
|
*/ |
305
|
|
|
public function getMethodName() |
306
|
|
|
{ |
307
|
|
|
return $this->methodName; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Return the primary name for this command. |
312
|
|
|
* |
313
|
|
|
* @return string |
314
|
|
|
*/ |
315
|
|
|
public function getName() |
316
|
|
|
{ |
317
|
|
|
$this->parseDocBlock(); |
318
|
|
|
return $this->name; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* Set the primary name for this command. |
323
|
|
|
* |
324
|
|
|
* @param string $name |
325
|
|
|
*/ |
326
|
|
|
public function setName($name) |
327
|
|
|
{ |
328
|
|
|
$this->name = $name; |
329
|
|
|
return $this; |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
public function getReturnType() |
333
|
|
|
{ |
334
|
|
|
$this->parseDocBlock(); |
335
|
|
|
return $this->returnType; |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
public function setReturnType($returnType) |
339
|
|
|
{ |
340
|
|
|
$this->returnType = $returnType; |
341
|
|
|
return $this; |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* Get any annotations included in the docblock comment for the |
346
|
|
|
* implementation method of this command that are not already |
347
|
|
|
* handled by the primary methods of this class. |
348
|
|
|
* |
349
|
|
|
* @return AnnotationData |
350
|
|
|
*/ |
351
|
|
|
public function getRawAnnotations() |
352
|
|
|
{ |
353
|
|
|
$this->parseDocBlock(); |
354
|
|
|
return $this->otherAnnotations; |
355
|
|
|
} |
356
|
|
|
|
357
|
|
|
/** |
358
|
|
|
* Get any annotations included in the docblock comment, |
359
|
|
|
* also including default values such as @command. We add |
360
|
|
|
* in the default @command annotation late, and only in a |
361
|
|
|
* copy of the annotation data because we use the existance |
362
|
|
|
* of a @command to indicate that this CommandInfo is |
363
|
|
|
* a command, and not a hook or anything else. |
364
|
|
|
* |
365
|
|
|
* @return AnnotationData |
366
|
|
|
*/ |
367
|
|
|
public function getAnnotations() |
368
|
|
|
{ |
369
|
|
|
// Also provide the path to the commandfile that |
370
|
|
|
// these annotations were pulled from. |
371
|
|
|
$path = $this->reflection->getFileName(); |
372
|
|
|
return new AnnotationData( |
373
|
|
|
$this->getRawAnnotations()->getArrayCopy() + |
374
|
|
|
[ |
375
|
|
|
'command' => $this->getName(), |
376
|
|
|
'_path' => $path, |
377
|
|
|
] |
378
|
|
|
); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
/** |
382
|
|
|
* Return a specific named annotation for this command. |
383
|
|
|
* |
384
|
|
|
* @param string $annotation The name of the annotation. |
385
|
|
|
* @return string |
386
|
|
|
*/ |
387
|
|
|
public function getAnnotation($annotation) |
388
|
|
|
{ |
389
|
|
|
// hasAnnotation parses the docblock |
390
|
|
|
if (!$this->hasAnnotation($annotation)) { |
391
|
|
|
return null; |
392
|
|
|
} |
393
|
|
|
return $this->otherAnnotations[$annotation]; |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
/** |
397
|
|
|
* Check to see if the specified annotation exists for this command. |
398
|
|
|
* |
399
|
|
|
* @param string $annotation The name of the annotation. |
400
|
|
|
* @return boolean |
401
|
|
|
*/ |
402
|
|
|
public function hasAnnotation($annotation) |
403
|
|
|
{ |
404
|
|
|
$this->parseDocBlock(); |
405
|
|
|
return isset($this->otherAnnotations[$annotation]); |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
/** |
409
|
|
|
* Save any tag that we do not explicitly recognize in the |
410
|
|
|
* 'otherAnnotations' map. |
411
|
|
|
*/ |
412
|
|
|
public function addAnnotation($name, $content) |
413
|
|
|
{ |
414
|
|
|
$this->otherAnnotations[$name] = $content; |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
/** |
418
|
|
|
* Remove an annotation that was previoudly set. |
419
|
|
|
*/ |
420
|
|
|
public function removeAnnotation($name) |
421
|
|
|
{ |
422
|
|
|
unset($this->otherAnnotations[$name]); |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
/** |
426
|
|
|
* Get the synopsis of the command (~first line). |
427
|
|
|
* |
428
|
|
|
* @return string |
429
|
|
|
*/ |
430
|
|
|
public function getDescription() |
431
|
|
|
{ |
432
|
|
|
$this->parseDocBlock(); |
433
|
|
|
return $this->description; |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
/** |
437
|
|
|
* Set the command description. |
438
|
|
|
* |
439
|
|
|
* @param string $description The description to set. |
440
|
|
|
*/ |
441
|
|
|
public function setDescription($description) |
442
|
|
|
{ |
443
|
|
|
$this->description = $description; |
444
|
|
|
return $this; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
/** |
448
|
|
|
* Get the help text of the command (the description) |
449
|
|
|
*/ |
450
|
|
|
public function getHelp() |
451
|
|
|
{ |
452
|
|
|
$this->parseDocBlock(); |
453
|
|
|
return $this->help; |
454
|
|
|
} |
455
|
|
|
/** |
456
|
|
|
* Set the help text for this command. |
457
|
|
|
* |
458
|
|
|
* @param string $help The help text. |
459
|
|
|
*/ |
460
|
|
|
public function setHelp($help) |
461
|
|
|
{ |
462
|
|
|
$this->help = $help; |
463
|
|
|
return $this; |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
/** |
467
|
|
|
* Return the list of aliases for this command. |
468
|
|
|
* @return string[] |
469
|
|
|
*/ |
470
|
|
|
public function getAliases() |
471
|
|
|
{ |
472
|
|
|
$this->parseDocBlock(); |
473
|
|
|
return $this->aliases; |
474
|
|
|
} |
475
|
|
|
|
476
|
|
|
/** |
477
|
|
|
* Set aliases that can be used in place of the command's primary name. |
478
|
|
|
* |
479
|
|
|
* @param string|string[] $aliases |
480
|
|
|
*/ |
481
|
|
|
public function setAliases($aliases) |
482
|
|
|
{ |
483
|
|
|
if (is_string($aliases)) { |
484
|
|
|
$aliases = explode(',', static::convertListToCommaSeparated($aliases)); |
485
|
|
|
} |
486
|
|
|
$this->aliases = array_filter($aliases); |
487
|
|
|
return $this; |
488
|
|
|
} |
489
|
|
|
|
490
|
|
|
/** |
491
|
|
|
* Return the examples for this command. This is @usage instead of |
492
|
|
|
* @example because the later is defined by the phpdoc standard to |
493
|
|
|
* be example method calls. |
494
|
|
|
* |
495
|
|
|
* @return string[] |
496
|
|
|
*/ |
497
|
|
|
public function getExampleUsages() |
498
|
|
|
{ |
499
|
|
|
$this->parseDocBlock(); |
500
|
|
|
return $this->exampleUsage; |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
/** |
504
|
|
|
* Add an example usage for this command. |
505
|
|
|
* |
506
|
|
|
* @param string $usage An example of the command, including the command |
507
|
|
|
* name and all of its example arguments and options. |
508
|
|
|
* @param string $description An explanation of what the example does. |
509
|
|
|
*/ |
510
|
|
|
public function setExampleUsage($usage, $description) |
511
|
|
|
{ |
512
|
|
|
$this->exampleUsage[$usage] = $description; |
513
|
|
|
return $this; |
514
|
|
|
} |
515
|
|
|
|
516
|
|
|
/** |
517
|
|
|
* Return the topics for this command. |
518
|
|
|
* |
519
|
|
|
* @return string[] |
520
|
|
|
*/ |
521
|
|
|
public function getTopics() |
522
|
|
|
{ |
523
|
|
|
if (!$this->hasAnnotation('topics')) { |
524
|
|
|
return []; |
525
|
|
|
} |
526
|
|
|
$topics = $this->getAnnotation('topics'); |
527
|
|
|
return explode(',', trim($topics)); |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
/** |
531
|
|
|
* Return the list of refleaction parameters. |
532
|
|
|
* |
533
|
|
|
* @return ReflectionParameter[] |
534
|
|
|
*/ |
535
|
|
|
public function getParameters() |
536
|
|
|
{ |
537
|
|
|
return $this->reflection->getParameters(); |
538
|
|
|
} |
539
|
|
|
|
540
|
|
|
/** |
541
|
|
|
* Descriptions of commandline arguements for this command. |
542
|
|
|
* |
543
|
|
|
* @return DefaultsWithDescriptions |
544
|
|
|
*/ |
545
|
|
|
public function arguments() |
546
|
|
|
{ |
547
|
|
|
return $this->arguments; |
548
|
|
|
} |
549
|
|
|
|
550
|
|
|
/** |
551
|
|
|
* Descriptions of commandline options for this command. |
552
|
|
|
* |
553
|
|
|
* @return DefaultsWithDescriptions |
554
|
|
|
*/ |
555
|
|
|
public function options() |
556
|
|
|
{ |
557
|
|
|
return $this->options; |
558
|
|
|
} |
559
|
|
|
|
560
|
|
|
/** |
561
|
|
|
* Get the inputOptions for the options associated with this CommandInfo |
562
|
|
|
* object, e.g. via @option annotations, or from |
563
|
|
|
* $options = ['someoption' => 'defaultvalue'] in the command method |
564
|
|
|
* parameter list. |
565
|
|
|
* |
566
|
|
|
* @return InputOption[] |
567
|
|
|
*/ |
568
|
|
|
public function inputOptions() |
569
|
|
|
{ |
570
|
|
|
if (!isset($this->inputOptions)) { |
571
|
|
|
$this->inputOptions = $this->createInputOptions(); |
572
|
|
|
} |
573
|
|
|
return $this->inputOptions; |
574
|
|
|
} |
575
|
|
|
|
576
|
|
|
protected function createInputOptions() |
577
|
|
|
{ |
578
|
|
|
$explicitOptions = []; |
579
|
|
|
|
580
|
|
|
$opts = $this->options()->getValues(); |
581
|
|
|
foreach ($opts as $name => $defaultValue) { |
582
|
|
|
$description = $this->options()->getDescription($name); |
583
|
|
|
|
584
|
|
|
$fullName = $name; |
585
|
|
|
$shortcut = ''; |
586
|
|
|
if (strpos($name, '|')) { |
587
|
|
|
list($fullName, $shortcut) = explode('|', $name, 2); |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
if (is_bool($defaultValue)) { |
591
|
|
|
$explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_NONE, $description); |
592
|
|
|
} elseif ($defaultValue === InputOption::VALUE_REQUIRED) { |
593
|
|
|
$explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_REQUIRED, $description); |
594
|
|
|
} elseif (is_array($defaultValue)) { |
595
|
|
|
$optionality = count($defaultValue) ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_REQUIRED; |
596
|
|
|
$explicitOptions[$fullName] = new InputOption( |
597
|
|
|
$fullName, |
598
|
|
|
$shortcut, |
599
|
|
|
InputOption::VALUE_IS_ARRAY | $optionality, |
600
|
|
|
$description, |
601
|
|
|
count($defaultValue) ? $defaultValue : null |
602
|
|
|
); |
603
|
|
|
} else { |
604
|
|
|
$explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $defaultValue); |
605
|
|
|
} |
606
|
|
|
} |
607
|
|
|
|
608
|
|
|
return $explicitOptions; |
609
|
|
|
} |
610
|
|
|
|
611
|
|
|
/** |
612
|
|
|
* An option might have a name such as 'silent|s'. In this |
613
|
|
|
* instance, we will allow the @option or @default tag to |
614
|
|
|
* reference the option only by name (e.g. 'silent' or 's' |
615
|
|
|
* instead of 'silent|s'). |
616
|
|
|
* |
617
|
|
|
* @param string $optionName |
618
|
|
|
* @return string |
619
|
|
|
*/ |
620
|
|
|
public function findMatchingOption($optionName) |
621
|
|
|
{ |
622
|
|
|
// Exit fast if there's an exact match |
623
|
|
|
if ($this->options->exists($optionName)) { |
624
|
|
|
return $optionName; |
625
|
|
|
} |
626
|
|
|
$existingOptionName = $this->findExistingOption($optionName); |
627
|
|
|
if (isset($existingOptionName)) { |
628
|
|
|
return $existingOptionName; |
629
|
|
|
} |
630
|
|
|
return $this->findOptionAmongAlternatives($optionName); |
631
|
|
|
} |
632
|
|
|
|
633
|
|
|
/** |
634
|
|
|
* @param string $optionName |
635
|
|
|
* @return string |
636
|
|
|
*/ |
637
|
|
|
protected function findOptionAmongAlternatives($optionName) |
638
|
|
|
{ |
639
|
|
|
// Check the other direction: if the annotation contains @silent|s |
640
|
|
|
// and the options array has 'silent|s'. |
641
|
|
|
$checkMatching = explode('|', $optionName); |
642
|
|
|
if (count($checkMatching) > 1) { |
643
|
|
|
foreach ($checkMatching as $checkName) { |
644
|
|
|
if ($this->options->exists($checkName)) { |
645
|
|
|
$this->options->rename($checkName, $optionName); |
646
|
|
|
return $optionName; |
647
|
|
|
} |
648
|
|
|
} |
649
|
|
|
} |
650
|
|
|
return $optionName; |
651
|
|
|
} |
652
|
|
|
|
653
|
|
|
/** |
654
|
|
|
* @param string $optionName |
655
|
|
|
* @return string|null |
656
|
|
|
*/ |
657
|
|
|
protected function findExistingOption($optionName) |
658
|
|
|
{ |
659
|
|
|
// Check to see if we can find the option name in an existing option, |
660
|
|
|
// e.g. if the options array has 'silent|s' => false, and the annotation |
661
|
|
|
// is @silent. |
662
|
|
|
foreach ($this->options()->getValues() as $name => $default) { |
663
|
|
|
if (in_array($optionName, explode('|', $name))) { |
664
|
|
|
return $name; |
665
|
|
|
} |
666
|
|
|
} |
667
|
|
|
} |
668
|
|
|
|
669
|
|
|
/** |
670
|
|
|
* Examine the parameters of the method for this command, and |
671
|
|
|
* build a list of commandline arguements for them. |
672
|
|
|
* |
673
|
|
|
* @return array |
674
|
|
|
*/ |
675
|
|
|
protected function determineAgumentClassifications() |
676
|
|
|
{ |
677
|
|
|
$result = new DefaultsWithDescriptions(); |
678
|
|
|
$params = $this->reflection->getParameters(); |
679
|
|
|
$optionsFromParameters = $this->determineOptionsFromParameters(); |
680
|
|
|
if (!empty($optionsFromParameters)) { |
681
|
|
|
array_pop($params); |
682
|
|
|
} |
683
|
|
|
foreach ($params as $param) { |
684
|
|
|
$this->addParameterToResult($result, $param); |
685
|
|
|
} |
686
|
|
|
return $result; |
687
|
|
|
} |
688
|
|
|
|
689
|
|
|
/** |
690
|
|
|
* Examine the provided parameter, and determine whether it |
691
|
|
|
* is a parameter that will be filled in with a positional |
692
|
|
|
* commandline argument. |
693
|
|
|
*/ |
694
|
|
|
protected function addParameterToResult($result, $param) |
695
|
|
|
{ |
696
|
|
|
// Commandline arguments must be strings, so ignore any |
697
|
|
|
// parameter that is typehinted to any non-primative class. |
698
|
|
|
if ($param->getClass() != null) { |
699
|
|
|
return; |
700
|
|
|
} |
701
|
|
|
$result->add($param->name); |
702
|
|
|
if ($param->isDefaultValueAvailable()) { |
703
|
|
|
$defaultValue = $param->getDefaultValue(); |
704
|
|
|
if (!$this->isAssoc($defaultValue)) { |
705
|
|
|
$result->setDefaultValue($param->name, $defaultValue); |
706
|
|
|
} |
707
|
|
|
} elseif ($param->isArray()) { |
708
|
|
|
$result->setDefaultValue($param->name, []); |
709
|
|
|
} |
710
|
|
|
} |
711
|
|
|
|
712
|
|
|
/** |
713
|
|
|
* Examine the parameters of the method for this command, and determine |
714
|
|
|
* the disposition of the options from them. |
715
|
|
|
* |
716
|
|
|
* @return array |
717
|
|
|
*/ |
718
|
|
|
protected function determineOptionsFromParameters() |
719
|
|
|
{ |
720
|
|
|
$params = $this->reflection->getParameters(); |
721
|
|
|
if (empty($params)) { |
722
|
|
|
return []; |
723
|
|
|
} |
724
|
|
|
$param = end($params); |
725
|
|
|
if (!$param->isDefaultValueAvailable()) { |
726
|
|
|
return []; |
727
|
|
|
} |
728
|
|
|
if (!$this->isAssoc($param->getDefaultValue())) { |
729
|
|
|
return []; |
730
|
|
|
} |
731
|
|
|
return $param->getDefaultValue(); |
732
|
|
|
} |
733
|
|
|
|
734
|
|
|
/** |
735
|
|
|
* Helper; determine if an array is associative or not. An array |
736
|
|
|
* is not associative if its keys are numeric, and numbered sequentially |
737
|
|
|
* from zero. All other arrays are considered to be associative. |
738
|
|
|
* |
739
|
|
|
* @param array $arr The array |
740
|
|
|
* @return boolean |
741
|
|
|
*/ |
742
|
|
|
protected function isAssoc($arr) |
743
|
|
|
{ |
744
|
|
|
if (!is_array($arr)) { |
745
|
|
|
return false; |
746
|
|
|
} |
747
|
|
|
return array_keys($arr) !== range(0, count($arr) - 1); |
748
|
|
|
} |
749
|
|
|
|
750
|
|
|
/** |
751
|
|
|
* Convert from a method name to the corresponding command name. A |
752
|
|
|
* method 'fooBar' will become 'foo:bar', and 'fooBarBazBoz' will |
753
|
|
|
* become 'foo:bar-baz-boz'. |
754
|
|
|
* |
755
|
|
|
* @param string $camel method name. |
756
|
|
|
* @return string |
757
|
|
|
*/ |
758
|
|
|
protected function convertName($camel) |
759
|
|
|
{ |
760
|
|
|
$splitter="-"; |
761
|
|
|
$camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel)); |
762
|
|
|
$camel = preg_replace("/$splitter/", ':', $camel, 1); |
763
|
|
|
return strtolower($camel); |
764
|
|
|
} |
765
|
|
|
|
766
|
|
|
/** |
767
|
|
|
* Parse the docBlock comment for this command, and set the |
768
|
|
|
* fields of this class with the data thereby obtained. |
769
|
|
|
*/ |
770
|
|
|
protected function parseDocBlock() |
771
|
|
|
{ |
772
|
|
|
if (!$this->docBlockIsParsed) { |
773
|
|
|
// The parse function will insert data from the provided method |
774
|
|
|
// into this object, using our accessors. |
775
|
|
|
CommandDocBlockParserFactory::parse($this, $this->reflection); |
776
|
|
|
$this->docBlockIsParsed = true; |
777
|
|
|
} |
778
|
|
|
} |
779
|
|
|
|
780
|
|
|
/** |
781
|
|
|
* Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c', |
782
|
|
|
* convert the data into the last of these forms. |
783
|
|
|
*/ |
784
|
|
|
protected static function convertListToCommaSeparated($text) |
785
|
|
|
{ |
786
|
|
|
return preg_replace('#[ \t\n\r,]+#', ',', $text); |
787
|
|
|
} |
788
|
|
|
} |
789
|
|
|
|
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.