annotate vendor/consolidation/annotated-command/src/Parser/CommandInfo.php @ 13:5fb285c0d0e3

Update Drupal core to 8.4.7 via Composer. Security update; I *think* we've been lucky to get away with this so far, as we don't support self-registration which seems to be used by the so-called "drupalgeddon 2" attack that 8.4.5 was vulnerable to.
author Chris Cannam
date Mon, 23 Apr 2018 09:33:26 +0100
parents 4c8ae668cc8c
children 129ea1e6d783
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@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 }