reflection = new \ReflectionMethod($classNameOrInstance, $methodName); $this->methodName = $methodName; $this->arguments = new DefaultsWithDescriptions(); $this->options = new DefaultsWithDescriptions(); // If the cache came from a newer version, ignore it and // regenerate the cached information. if (!empty($cache) && CommandInfoDeserializer::isValidSerializedData($cache) && !$this->cachedFileIsModified($cache)) { $deserializer = new CommandInfoDeserializer(); $deserializer->constructFromCache($this, $cache); $this->docBlockIsParsed = true; } else { $this->constructFromClassAndMethod($classNameOrInstance, $methodName); } } public static function create($classNameOrInstance, $methodName) { return new self($classNameOrInstance, $methodName); } public static function deserialize($cache) { $cache = (array)$cache; return new self($cache['class'], $cache['method_name'], $cache); } public function cachedFileIsModified($cache) { $path = $this->reflection->getFileName(); return filemtime($path) != $cache['mtime']; } protected function constructFromClassAndMethod($classNameOrInstance, $methodName) { $this->otherAnnotations = new AnnotationData(); // Set up a default name for the command from the method name. // This can be overridden via @command or @name annotations. $this->name = $this->convertName($methodName); $this->options = new DefaultsWithDescriptions($this->determineOptionsFromParameters(), false); $this->arguments = $this->determineAgumentClassifications(); } /** * Recover the method name provided to the constructor. * * @return string */ public function getMethodName() { return $this->methodName; } /** * Return the primary name for this command. * * @return string */ public function getName() { $this->parseDocBlock(); return $this->name; } /** * Set the primary name for this command. * * @param string $name */ public function setName($name) { $this->name = $name; return $this; } /** * Return whether or not this method represents a valid command * or hook. */ public function valid() { return !empty($this->name); } /** * If higher-level code decides that this CommandInfo is not interesting * or useful (if it is not a command method or a hook method), then * we will mark it as invalid to prevent it from being created as a command. * We still cache a placeholder record for invalid methods, so that we * do not need to re-parse the method again later simply to determine that * it is invalid. */ public function invalidate() { $this->name = ''; } public function getReturnType() { $this->parseDocBlock(); return $this->returnType; } public function setReturnType($returnType) { $this->returnType = $returnType; return $this; } /** * Get any annotations included in the docblock comment for the * implementation method of this command that are not already * handled by the primary methods of this class. * * @return AnnotationData */ public function getRawAnnotations() { $this->parseDocBlock(); return $this->otherAnnotations; } /** * Replace the annotation data. */ public function replaceRawAnnotations($annotationData) { $this->otherAnnotations = new AnnotationData((array) $annotationData); return $this; } /** * Get any annotations included in the docblock comment, * also including default values such as @command. We add * in the default @command annotation late, and only in a * copy of the annotation data because we use the existance * of a @command to indicate that this CommandInfo is * a command, and not a hook or anything else. * * @return AnnotationData */ public function getAnnotations() { // Also provide the path to the commandfile that these annotations // were pulled from and the classname of that file. $path = $this->reflection->getFileName(); $className = $this->reflection->getDeclaringClass()->getName(); return new AnnotationData( $this->getRawAnnotations()->getArrayCopy() + [ 'command' => $this->getName(), '_path' => $path, '_classname' => $className, ] ); } /** * Return a specific named annotation for this command as a list. * * @param string $name The name of the annotation. * @return array|null */ public function getAnnotationList($name) { // hasAnnotation parses the docblock if (!$this->hasAnnotation($name)) { return null; } return $this->otherAnnotations->getList($name); ; } /** * Return a specific named annotation for this command as a string. * * @param string $name The name of the annotation. * @return string|null */ public function getAnnotation($name) { // hasAnnotation parses the docblock if (!$this->hasAnnotation($name)) { return null; } return $this->otherAnnotations->get($name); } /** * Check to see if the specified annotation exists for this command. * * @param string $annotation The name of the annotation. * @return boolean */ public function hasAnnotation($annotation) { $this->parseDocBlock(); return isset($this->otherAnnotations[$annotation]); } /** * Save any tag that we do not explicitly recognize in the * 'otherAnnotations' map. */ public function addAnnotation($name, $content) { // Convert to an array and merge if there are multiple // instances of the same annotation defined. if (isset($this->otherAnnotations[$name])) { $content = array_merge((array) $this->otherAnnotations[$name], (array)$content); } $this->otherAnnotations[$name] = $content; } /** * Remove an annotation that was previoudly set. */ public function removeAnnotation($name) { unset($this->otherAnnotations[$name]); } /** * Get the synopsis of the command (~first line). * * @return string */ public function getDescription() { $this->parseDocBlock(); return $this->description; } /** * Set the command description. * * @param string $description The description to set. */ public function setDescription($description) { $this->description = str_replace("\n", ' ', $description); return $this; } /** * Get the help text of the command (the description) */ public function getHelp() { $this->parseDocBlock(); return $this->help; } /** * Set the help text for this command. * * @param string $help The help text. */ public function setHelp($help) { $this->help = $help; return $this; } /** * Return the list of aliases for this command. * @return string[] */ public function getAliases() { $this->parseDocBlock(); return $this->aliases; } /** * Set aliases that can be used in place of the command's primary name. * * @param string|string[] $aliases */ public function setAliases($aliases) { if (is_string($aliases)) { $aliases = explode(',', static::convertListToCommaSeparated($aliases)); } $this->aliases = array_filter($aliases); return $this; } /** * Get hidden status for the command. * @return bool */ public function getHidden() { $this->parseDocBlock(); return $this->hasAnnotation('hidden'); } /** * Set hidden status. List command omits hidden commands. * * @param bool $hidden */ public function setHidden($hidden) { $this->hidden = $hidden; return $this; } /** * Return the examples for this command. This is @usage instead of * @example because the later is defined by the phpdoc standard to * be example method calls. * * @return string[] */ public function getExampleUsages() { $this->parseDocBlock(); return $this->exampleUsage; } /** * Add an example usage for this command. * * @param string $usage An example of the command, including the command * name and all of its example arguments and options. * @param string $description An explanation of what the example does. */ public function setExampleUsage($usage, $description) { $this->exampleUsage[$usage] = $description; return $this; } /** * Overwrite all example usages */ public function replaceExampleUsages($usages) { $this->exampleUsage = $usages; return $this; } /** * Return the topics for this command. * * @return string[] */ public function getTopics() { if (!$this->hasAnnotation('topics')) { return []; } $topics = $this->getAnnotation('topics'); return explode(',', trim($topics)); } /** * Return the list of refleaction parameters. * * @return ReflectionParameter[] */ public function getParameters() { return $this->reflection->getParameters(); } /** * Descriptions of commandline arguements for this command. * * @return DefaultsWithDescriptions */ public function arguments() { return $this->arguments; } /** * Descriptions of commandline options for this command. * * @return DefaultsWithDescriptions */ public function options() { return $this->options; } /** * Get the inputOptions for the options associated with this CommandInfo * object, e.g. via @option annotations, or from * $options = ['someoption' => 'defaultvalue'] in the command method * parameter list. * * @return InputOption[] */ public function inputOptions() { if (!isset($this->inputOptions)) { $this->inputOptions = $this->createInputOptions(); } return $this->inputOptions; } protected function addImplicitNoOptions() { $opts = $this->options()->getValues(); foreach ($opts as $name => $defaultValue) { if ($defaultValue === true) { $key = 'no-' . $name; if (!array_key_exists($key, $opts)) { $description = "Negate --$name option."; $this->options()->add($key, $description, false); } } } } protected function createInputOptions() { $explicitOptions = []; $this->addImplicitNoOptions(); $opts = $this->options()->getValues(); foreach ($opts as $name => $defaultValue) { $description = $this->options()->getDescription($name); $fullName = $name; $shortcut = ''; if (strpos($name, '|')) { list($fullName, $shortcut) = explode('|', $name, 2); } // Treat the following two cases identically: // - 'foo' => InputOption::VALUE_OPTIONAL // - 'foo' => null // The first form is preferred, but we will convert the value // to 'null' for storage as the option default value. if ($defaultValue === InputOption::VALUE_OPTIONAL) { $defaultValue = null; } if ($defaultValue === false) { $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_NONE, $description); } elseif ($defaultValue === InputOption::VALUE_REQUIRED) { $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_REQUIRED, $description); } elseif (is_array($defaultValue)) { $optionality = count($defaultValue) ? InputOption::VALUE_OPTIONAL : InputOption::VALUE_REQUIRED; $explicitOptions[$fullName] = new InputOption( $fullName, $shortcut, InputOption::VALUE_IS_ARRAY | $optionality, $description, count($defaultValue) ? $defaultValue : null ); } else { $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $defaultValue); } } return $explicitOptions; } /** * An option might have a name such as 'silent|s'. In this * instance, we will allow the @option or @default tag to * reference the option only by name (e.g. 'silent' or 's' * instead of 'silent|s'). * * @param string $optionName * @return string */ public function findMatchingOption($optionName) { // Exit fast if there's an exact match if ($this->options->exists($optionName)) { return $optionName; } $existingOptionName = $this->findExistingOption($optionName); if (isset($existingOptionName)) { return $existingOptionName; } return $this->findOptionAmongAlternatives($optionName); } /** * @param string $optionName * @return string */ protected function findOptionAmongAlternatives($optionName) { // Check the other direction: if the annotation contains @silent|s // and the options array has 'silent|s'. $checkMatching = explode('|', $optionName); if (count($checkMatching) > 1) { foreach ($checkMatching as $checkName) { if ($this->options->exists($checkName)) { $this->options->rename($checkName, $optionName); return $optionName; } } } return $optionName; } /** * @param string $optionName * @return string|null */ protected function findExistingOption($optionName) { // Check to see if we can find the option name in an existing option, // e.g. if the options array has 'silent|s' => false, and the annotation // is @silent. foreach ($this->options()->getValues() as $name => $default) { if (in_array($optionName, explode('|', $name))) { return $name; } } } /** * Examine the parameters of the method for this command, and * build a list of commandline arguements for them. * * @return array */ protected function determineAgumentClassifications() { $result = new DefaultsWithDescriptions(); $params = $this->reflection->getParameters(); $optionsFromParameters = $this->determineOptionsFromParameters(); if ($this->lastParameterIsOptionsArray()) { array_pop($params); } foreach ($params as $param) { $this->addParameterToResult($result, $param); } return $result; } /** * Examine the provided parameter, and determine whether it * is a parameter that will be filled in with a positional * commandline argument. */ protected function addParameterToResult($result, $param) { // Commandline arguments must be strings, so ignore any // parameter that is typehinted to any non-primative class. if ($param->getClass() != null) { return; } $result->add($param->name); if ($param->isDefaultValueAvailable()) { $defaultValue = $param->getDefaultValue(); if (!$this->isAssoc($defaultValue)) { $result->setDefaultValue($param->name, $defaultValue); } } elseif ($param->isArray()) { $result->setDefaultValue($param->name, []); } } /** * Examine the parameters of the method for this command, and determine * the disposition of the options from them. * * @return array */ protected function determineOptionsFromParameters() { $params = $this->reflection->getParameters(); if (empty($params)) { return []; } $param = end($params); if (!$param->isDefaultValueAvailable()) { return []; } if (!$this->isAssoc($param->getDefaultValue())) { return []; } return $param->getDefaultValue(); } /** * Determine if the last argument contains $options. * * Two forms indicate options: * - $options = [] * - $options = ['flag' => 'default-value'] * * Any other form, including `array $foo`, is not options. */ protected function lastParameterIsOptionsArray() { $params = $this->reflection->getParameters(); if (empty($params)) { return []; } $param = end($params); if (!$param->isDefaultValueAvailable()) { return []; } return is_array($param->getDefaultValue()); } /** * Helper; determine if an array is associative or not. An array * is not associative if its keys are numeric, and numbered sequentially * from zero. All other arrays are considered to be associative. * * @param array $arr The array * @return boolean */ protected function isAssoc($arr) { if (!is_array($arr)) { return false; } return array_keys($arr) !== range(0, count($arr) - 1); } /** * Convert from a method name to the corresponding command name. A * method 'fooBar' will become 'foo:bar', and 'fooBarBazBoz' will * become 'foo:bar-baz-boz'. * * @param string $camel method name. * @return string */ protected function convertName($camel) { $splitter="-"; $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel)); $camel = preg_replace("/$splitter/", ':', $camel, 1); return strtolower($camel); } /** * Parse the docBlock comment for this command, and set the * fields of this class with the data thereby obtained. */ protected function parseDocBlock() { if (!$this->docBlockIsParsed) { // The parse function will insert data from the provided method // into this object, using our accessors. CommandDocBlockParserFactory::parse($this, $this->reflection); $this->docBlockIsParsed = true; } } /** * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c', * convert the data into the last of these forms. */ protected static function convertListToCommaSeparated($text) { return preg_replace('#[ \t\n\r,]+#', ',', $text); } }