'processCommandTag', 'name' => 'processCommandTag', 'arg' => 'processArgumentTag', 'param' => 'processParamTag', 'return' => 'processReturnTag', 'option' => 'processOptionTag', 'default' => 'processDefaultTag', 'aliases' => 'processAliases', 'usage' => 'processUsageTag', 'description' => 'processAlternateDescriptionTag', 'desc' => 'processAlternateDescriptionTag', ]; public function __construct(CommandInfo $commandInfo, \ReflectionMethod $reflection) { $this->commandInfo = $commandInfo; $this->reflection = $reflection; } protected function processAllTags($phpdoc) { // Iterate over all of the tags, and process them as necessary. foreach ($phpdoc->getTags() as $tag) { $processFn = [$this, 'processGenericTag']; if (array_key_exists($tag->getName(), $this->tagProcessors)) { $processFn = [$this, $this->tagProcessors[$tag->getName()]]; } $processFn($tag); } } abstract protected function getTagContents($tag); /** * Parse the docBlock comment for this command, and set the * fields of this class with the data thereby obtained. */ abstract public function parse(); /** * Save any tag that we do not explicitly recognize in the * 'otherAnnotations' map. */ protected function processGenericTag($tag) { $this->commandInfo->addAnnotation($tag->getName(), $this->getTagContents($tag)); } /** * Set the name of the command from a @command or @name annotation. */ protected function processCommandTag($tag) { $commandName = $this->getTagContents($tag); $this->commandInfo->setName($commandName); // We also store the name in the 'other annotations' so that is is // possible to determine if the method had a @command annotation. $this->commandInfo->addAnnotation($tag->getName(), $commandName); } /** * The @description and @desc annotations may be used in * place of the synopsis (which we call 'description'). * This is discouraged. * * @deprecated */ protected function processAlternateDescriptionTag($tag) { $this->commandInfo->setDescription($this->getTagContents($tag)); } /** * Store the data from a @arg annotation in our argument descriptions. */ protected function processArgumentTag($tag) { if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) { return; } $this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments(), $match); } /** * Store the data from an @option annotation in our option descriptions. */ protected function processOptionTag($tag) { if (!$this->pregMatchOptionNameAndDescription((string)$tag->getDescription(), $match)) { return; } $this->addOptionOrArgumentTag($tag, $this->commandInfo->options(), $match); } protected function addOptionOrArgumentTag($tag, DefaultsWithDescriptions $set, $nameAndDescription) { $variableName = $this->commandInfo->findMatchingOption($nameAndDescription['name']); $desc = $nameAndDescription['description']; $description = static::removeLineBreaks($desc); $set->add($variableName, $description); } /** * Store the data from a @default annotation in our argument or option store, * as appropriate. */ protected function processDefaultTag($tag) { if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) { return; } $variableName = $match['name']; $defaultValue = $this->interpretDefaultValue($match['description']); if ($this->commandInfo->arguments()->exists($variableName)) { $this->commandInfo->arguments()->setDefaultValue($variableName, $defaultValue); return; } $variableName = $this->commandInfo->findMatchingOption($variableName); if ($this->commandInfo->options()->exists($variableName)) { $this->commandInfo->options()->setDefaultValue($variableName, $defaultValue); } } /** * Store the data from a @usage annotation in our example usage list. */ protected function processUsageTag($tag) { $lines = explode("\n", $this->getTagContents($tag)); $usage = array_shift($lines); $description = static::removeLineBreaks(implode("\n", $lines)); $this->commandInfo->setExampleUsage($usage, $description); } /** * Process the comma-separated list of aliases */ protected function processAliases($tag) { $this->commandInfo->setAliases((string)$tag->getDescription()); } protected function lastParameterName() { $params = $this->commandInfo->getParameters(); $param = end($params); if (!$param) { return ''; } return $param->name; } /** * Return the name of the last parameter if it holds the options. */ public function optionParamName() { // Remember the name of the last parameter, if it holds the options. // We will use this information to ignore @param annotations for the options. if (!isset($this->optionParamName)) { $this->optionParamName = ''; $options = $this->commandInfo->options(); if (!$options->isEmpty()) { $this->optionParamName = $this->lastParameterName(); } } return $this->optionParamName; } /** * Store the data from a @param annotation in our argument descriptions. */ protected function processParamTag($tag) { $variableName = $tag->getVariableName(); $variableName = str_replace('$', '', $variableName); $description = static::removeLineBreaks((string)$tag->getDescription()); if ($variableName == $this->optionParamName()) { return; } $this->commandInfo->arguments()->add($variableName, $description); } /** * Store the data from a @return annotation in our argument descriptions. */ abstract protected function processReturnTag($tag); protected function interpretDefaultValue($defaultValue) { $defaults = [ 'null' => null, 'true' => true, 'false' => false, "''" => '', '[]' => [], ]; foreach ($defaults as $defaultName => $defaultTypedValue) { if ($defaultValue == $defaultName) { return $defaultTypedValue; } } return $defaultValue; } /** * Given a docblock description in the form "$variable description", * return the variable name and description via the 'match' parameter. */ protected function pregMatchNameAndDescription($source, &$match) { $nameRegEx = '\\$(?P[^ \t]+)[ \t]+'; $descriptionRegEx = '(?P.*)'; $optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s"; return preg_match($optionRegEx, $source, $match); } /** * Given a docblock description in the form "$variable description", * return the variable name and description via the 'match' parameter. */ protected function pregMatchOptionNameAndDescription($source, &$match) { // Strip type and $ from the text before the @option name, if present. $source = preg_replace('/^[a-zA-Z]* ?\\$/', '', $source); $nameRegEx = '(?P[^ \t]+)[ \t]+'; $descriptionRegEx = '(?P.*)'; $optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s"; return preg_match($optionRegEx, $source, $match); } /** * 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); } /** * Take a multiline description and convert it into a single * long unbroken line. */ protected static function removeLineBreaks($text) { return trim(preg_replace('#[ \t\n\r]+#', ' ', $text)); } }