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