annotate vendor/consolidation/annotated-command/src/Parser/CommandInfo.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
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 }