Chris@0: Chris@0: * @author Justin Hileman Chris@0: */ Chris@0: class Docblock Chris@0: { Chris@0: /** Chris@0: * Tags in the docblock that have a whitespace-delimited number of parameters Chris@0: * (such as `@param type var desc` and `@return type desc`) and the names of Chris@0: * those parameters. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: public static $vectors = array( Chris@0: 'throws' => array('type', 'desc'), Chris@0: 'param' => array('type', 'var', 'desc'), Chris@0: 'return' => array('type', 'desc'), Chris@0: ); Chris@0: Chris@0: protected $reflector; Chris@0: Chris@0: /** Chris@0: * The description of the symbol. Chris@0: * Chris@0: * @var string Chris@0: */ Chris@0: public $desc; Chris@0: Chris@0: /** Chris@0: * The tags defined in the docblock. Chris@0: * Chris@0: * The array has keys which are the tag names (excluding the @) and values Chris@0: * that are arrays, each of which is an entry for the tag. Chris@0: * Chris@0: * In the case where the tag name is defined in {@see DocBlock::$vectors} the Chris@0: * value within the tag-value array is an array in itself with keys as Chris@0: * described by {@see DocBlock::$vectors}. Chris@0: * Chris@0: * @var array Chris@0: */ Chris@0: public $tags; Chris@0: Chris@0: /** Chris@0: * The entire DocBlock comment that was parsed. Chris@0: * Chris@0: * @var string Chris@0: */ Chris@0: public $comment; Chris@0: Chris@0: /** Chris@0: * Docblock constructor. Chris@0: * Chris@0: * @param \Reflector $reflector Chris@0: */ Chris@0: public function __construct(\Reflector $reflector) Chris@0: { Chris@0: $this->reflector = $reflector; Chris@0: $this->setComment($reflector->getDocComment()); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set and parse the docblock comment. Chris@0: * Chris@0: * @param string $comment The docblock Chris@0: */ Chris@0: protected function setComment($comment) Chris@0: { Chris@0: $this->desc = ''; Chris@0: $this->tags = array(); Chris@0: $this->comment = $comment; Chris@0: Chris@0: $this->parseComment($comment); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Find the length of the docblock prefix. Chris@0: * Chris@0: * @param array $lines Chris@0: * Chris@0: * @return int Prefix length Chris@0: */ Chris@0: protected static function prefixLength(array $lines) Chris@0: { Chris@0: // find only lines with interesting things Chris@0: $lines = array_filter($lines, function ($line) { Chris@0: return substr($line, strspn($line, "* \t\n\r\0\x0B")); Chris@0: }); Chris@0: Chris@0: // if we sort the lines, we only have to compare two items Chris@0: sort($lines); Chris@0: Chris@0: $first = reset($lines); Chris@0: $last = end($lines); Chris@0: Chris@0: // find the longest common substring Chris@0: $count = min(strlen($first), strlen($last)); Chris@0: for ($i = 0; $i < $count; $i++) { Chris@0: if ($first[$i] !== $last[$i]) { Chris@0: return $i; Chris@0: } Chris@0: } Chris@0: Chris@0: return $count; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parse the comment into the component parts and set the state of the object. Chris@0: * Chris@0: * @param string $comment The docblock Chris@0: */ Chris@0: protected function parseComment($comment) Chris@0: { Chris@0: // Strip the opening and closing tags of the docblock Chris@0: $comment = substr($comment, 3, -2); Chris@0: Chris@0: // Split into arrays of lines Chris@0: $comment = array_filter(preg_split('/\r?\n\r?/', $comment)); Chris@0: Chris@0: // Trim asterisks and whitespace from the beginning and whitespace from the end of lines Chris@0: $prefixLength = self::prefixLength($comment); Chris@0: $comment = array_map(function ($line) use ($prefixLength) { Chris@0: return rtrim(substr($line, $prefixLength)); Chris@0: }, $comment); Chris@0: Chris@0: // Group the lines together by @tags Chris@0: $blocks = array(); Chris@0: $b = -1; Chris@0: foreach ($comment as $line) { Chris@0: if (self::isTagged($line)) { Chris@0: $b++; Chris@0: $blocks[] = array(); Chris@0: } elseif ($b === -1) { Chris@0: $b = 0; Chris@0: $blocks[] = array(); Chris@0: } Chris@0: $blocks[$b][] = $line; Chris@0: } Chris@0: Chris@0: // Parse the blocks Chris@0: foreach ($blocks as $block => $body) { Chris@0: $body = trim(implode("\n", $body)); Chris@0: Chris@0: if ($block === 0 && !self::isTagged($body)) { Chris@0: // This is the description block Chris@0: $this->desc = $body; Chris@0: } else { Chris@0: // This block is tagged Chris@0: $tag = substr(self::strTag($body), 1); Chris@0: $body = ltrim(substr($body, strlen($tag) + 2)); Chris@0: Chris@0: if (isset(self::$vectors[$tag])) { Chris@0: // The tagged block is a vector Chris@0: $count = count(self::$vectors[$tag]); Chris@0: if ($body) { Chris@0: $parts = preg_split('/\s+/', $body, $count); Chris@0: } else { Chris@0: $parts = array(); Chris@0: } Chris@0: Chris@0: // Default the trailing values Chris@0: $parts = array_pad($parts, $count, null); Chris@0: Chris@0: // Store as a mapped array Chris@0: $this->tags[$tag][] = array_combine(self::$vectors[$tag], $parts); Chris@0: } else { Chris@0: // The tagged block is only text Chris@0: $this->tags[$tag][] = $body; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Whether or not a docblock contains a given @tag. Chris@0: * Chris@0: * @param string $tag The name of the @tag to check for Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: public function hasTag($tag) Chris@0: { Chris@0: return is_array($this->tags) && array_key_exists($tag, $this->tags); Chris@0: } Chris@0: Chris@0: /** Chris@0: * The value of a tag. Chris@0: * Chris@0: * @param string $tag Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: public function tag($tag) Chris@0: { Chris@0: return $this->hasTag($tag) ? $this->tags[$tag] : null; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Whether or not a string begins with a @tag. Chris@0: * Chris@0: * @param string $str Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: public static function isTagged($str) Chris@0: { Chris@0: return isset($str[1]) && $str[0] === '@' && ctype_alpha($str[1]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * The tag at the beginning of a string. Chris@0: * Chris@0: * @param string $str Chris@0: * Chris@0: * @return string|null Chris@0: */ Chris@0: public static function strTag($str) Chris@0: { Chris@0: if (preg_match('/^@[a-z0-9_]+/', $str, $matches)) { Chris@0: return $matches[0]; Chris@0: } Chris@0: } Chris@0: }