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