Chris@0: Chris@0: * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) Chris@0: * @license http://www.opensource.org/licenses/mit-license.php MIT Chris@0: * @link http://phpdoc.org Chris@0: */ Chris@0: Chris@0: namespace phpDocumentor\Reflection; Chris@0: Chris@0: use phpDocumentor\Reflection\DocBlock\Tag; Chris@0: use phpDocumentor\Reflection\DocBlock\Context; Chris@0: use phpDocumentor\Reflection\DocBlock\Location; Chris@0: Chris@0: /** Chris@0: * Parses the DocBlock for any structure. Chris@0: * Chris@0: * @author Mike van Riel Chris@0: * @license http://www.opensource.org/licenses/mit-license.php MIT Chris@0: * @link http://phpdoc.org Chris@0: */ Chris@0: class DocBlock implements \Reflector Chris@0: { Chris@0: /** @var string The opening line for this docblock. */ Chris@0: protected $short_description = ''; Chris@0: Chris@0: /** Chris@0: * @var DocBlock\Description The actual Chris@0: * description for this docblock. Chris@0: */ Chris@0: protected $long_description = null; Chris@0: Chris@0: /** Chris@0: * @var Tag[] An array containing all Chris@0: * the tags in this docblock; except inline. Chris@0: */ Chris@0: protected $tags = array(); Chris@0: Chris@0: /** @var Context Information about the context of this DocBlock. */ Chris@0: protected $context = null; Chris@0: Chris@0: /** @var Location Information about the location of this DocBlock. */ Chris@0: protected $location = null; Chris@0: Chris@0: /** @var bool Is this DocBlock (the start of) a template? */ Chris@0: protected $isTemplateStart = false; Chris@0: Chris@0: /** @var bool Does this DocBlock signify the end of a DocBlock template? */ Chris@0: protected $isTemplateEnd = false; Chris@0: Chris@0: /** Chris@0: * Parses the given docblock and populates the member fields. Chris@0: * Chris@0: * The constructor may also receive namespace information such as the Chris@0: * current namespace and aliases. This information is used by some tags Chris@0: * (e.g. @return, @param, etc.) to turn a relative Type into a FQCN. Chris@0: * Chris@0: * @param \Reflector|string $docblock A docblock comment (including Chris@0: * asterisks) or reflector supporting the getDocComment method. Chris@0: * @param Context $context The context in which the DocBlock Chris@0: * occurs. Chris@0: * @param Location $location The location within the file that this Chris@0: * DocBlock occurs in. Chris@0: * Chris@0: * @throws \InvalidArgumentException if the given argument does not have the Chris@0: * getDocComment method. Chris@0: */ Chris@0: public function __construct( Chris@0: $docblock, Chris@0: Context $context = null, Chris@0: Location $location = null Chris@0: ) { Chris@0: if (is_object($docblock)) { Chris@0: if (!method_exists($docblock, 'getDocComment')) { Chris@0: throw new \InvalidArgumentException( Chris@0: 'Invalid object passed; the given reflector must support ' Chris@0: . 'the getDocComment method' Chris@0: ); Chris@0: } Chris@0: Chris@0: $docblock = $docblock->getDocComment(); Chris@0: } Chris@0: Chris@0: $docblock = $this->cleanInput($docblock); Chris@0: Chris@0: list($templateMarker, $short, $long, $tags) = $this->splitDocBlock($docblock); Chris@0: $this->isTemplateStart = $templateMarker === '#@+'; Chris@0: $this->isTemplateEnd = $templateMarker === '#@-'; Chris@0: $this->short_description = $short; Chris@0: $this->long_description = new DocBlock\Description($long, $this); Chris@0: $this->parseTags($tags); Chris@0: Chris@0: $this->context = $context; Chris@0: $this->location = $location; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Strips the asterisks from the DocBlock comment. Chris@0: * Chris@0: * @param string $comment String containing the comment text. Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: protected function cleanInput($comment) Chris@0: { Chris@0: $comment = trim( Chris@0: preg_replace( Chris@0: '#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]{0,1}(.*)?#u', Chris@0: '$1', Chris@0: $comment Chris@0: ) Chris@0: ); Chris@0: Chris@0: // reg ex above is not able to remove */ from a single line docblock Chris@0: if (substr($comment, -2) == '*/') { Chris@0: $comment = trim(substr($comment, 0, -2)); Chris@0: } Chris@0: Chris@0: // normalize strings Chris@0: $comment = str_replace(array("\r\n", "\r"), "\n", $comment); Chris@0: Chris@0: return $comment; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Splits the DocBlock into a template marker, summary, description and block of tags. Chris@0: * Chris@0: * @param string $comment Comment to split into the sub-parts. Chris@0: * Chris@0: * @author Richard van Velzen (@_richardJ) Special thanks to Richard for the regex responsible for the split. Chris@0: * @author Mike van Riel for extending the regex with template marker support. Chris@0: * Chris@0: * @return string[] containing the template marker (if any), summary, description and a string containing the tags. Chris@0: */ Chris@0: protected function splitDocBlock($comment) Chris@0: { Chris@0: // Performance improvement cheat: if the first character is an @ then only tags are in this DocBlock. This Chris@0: // method does not split tags so we return this verbatim as the fourth result (tags). This saves us the Chris@0: // performance impact of running a regular expression Chris@0: if (strpos($comment, '@') === 0) { Chris@0: return array('', '', '', $comment); Chris@0: } Chris@0: Chris@0: // clears all extra horizontal whitespace from the line endings to prevent parsing issues Chris@0: $comment = preg_replace('/\h*$/Sum', '', $comment); Chris@0: Chris@0: /* Chris@0: * Splits the docblock into a template marker, short description, long description and tags section Chris@0: * Chris@0: * - The template marker is empty, #@+ or #@- if the DocBlock starts with either of those (a newline may Chris@0: * occur after it and will be stripped). Chris@0: * - The short description is started from the first character until a dot is encountered followed by a Chris@0: * newline OR two consecutive newlines (horizontal whitespace is taken into account to consider spacing Chris@0: * errors). This is optional. Chris@0: * - The long description, any character until a new line is encountered followed by an @ and word Chris@0: * characters (a tag). This is optional. Chris@0: * - Tags; the remaining characters Chris@0: * Chris@0: * Big thanks to RichardJ for contributing this Regular Expression Chris@0: */ Chris@0: preg_match( Chris@0: '/ Chris@0: \A Chris@0: # 1. Extract the template marker Chris@0: (?:(\#\@\+|\#\@\-)\n?)? Chris@0: Chris@0: # 2. Extract the summary Chris@0: (?: Chris@0: (?! @\pL ) # The summary may not start with an @ Chris@0: ( Chris@0: [^\n.]+ Chris@0: (?: Chris@0: (?! \. \n | \n{2} ) # End summary upon a dot followed by newline or two newlines Chris@0: [\n.] (?! [ \t]* @\pL ) # End summary when an @ is found as first character on a new line Chris@0: [^\n.]+ # Include anything else Chris@0: )* Chris@0: \.? Chris@0: )? Chris@0: ) Chris@0: Chris@0: # 3. Extract the description Chris@0: (?: Chris@0: \s* # Some form of whitespace _must_ precede a description because a summary must be there Chris@0: (?! @\pL ) # The description may not start with an @ Chris@0: ( Chris@0: [^\n]+ Chris@0: (?: \n+ Chris@0: (?! [ \t]* @\pL ) # End description when an @ is found as first character on a new line Chris@0: [^\n]+ # Include anything else Chris@0: )* Chris@0: ) Chris@0: )? Chris@0: Chris@0: # 4. Extract the tags (anything that follows) Chris@0: (\s+ [\s\S]*)? # everything that follows Chris@0: /ux', Chris@0: $comment, Chris@0: $matches Chris@0: ); Chris@0: array_shift($matches); Chris@0: Chris@0: while (count($matches) < 4) { Chris@0: $matches[] = ''; Chris@0: } Chris@0: Chris@0: return $matches; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Creates the tag objects. Chris@0: * Chris@0: * @param string $tags Tag block to parse. Chris@0: * Chris@0: * @return void Chris@0: */ Chris@0: protected function parseTags($tags) Chris@0: { Chris@0: $result = array(); Chris@0: $tags = trim($tags); Chris@0: if ('' !== $tags) { Chris@0: if ('@' !== $tags[0]) { Chris@0: throw new \LogicException( Chris@0: 'A tag block started with text instead of an actual tag,' Chris@0: . ' this makes the tag block invalid: ' . $tags Chris@0: ); Chris@0: } Chris@0: foreach (explode("\n", $tags) as $tag_line) { Chris@0: if (isset($tag_line[0]) && ($tag_line[0] === '@')) { Chris@0: $result[] = $tag_line; Chris@0: } else { Chris@0: $result[count($result) - 1] .= "\n" . $tag_line; Chris@0: } Chris@0: } Chris@0: Chris@0: // create proper Tag objects Chris@0: foreach ($result as $key => $tag_line) { Chris@0: $result[$key] = Tag::createInstance(trim($tag_line), $this); Chris@0: } Chris@0: } Chris@0: Chris@0: $this->tags = $result; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the text portion of the doc block. Chris@0: * Chris@0: * Gets the text portion (short and long description combined) of the doc Chris@0: * block. Chris@0: * Chris@0: * @return string The text portion of the doc block. Chris@0: */ Chris@0: public function getText() Chris@0: { Chris@0: $short = $this->getShortDescription(); Chris@0: $long = $this->getLongDescription()->getContents(); Chris@0: Chris@0: if ($long) { Chris@0: return "{$short}\n\n{$long}"; Chris@0: } else { Chris@0: return $short; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Set the text portion of the doc block. Chris@0: * Chris@0: * Sets the text portion (short and long description combined) of the doc Chris@0: * block. Chris@0: * Chris@0: * @param string $docblock The new text portion of the doc block. Chris@0: * Chris@0: * @return $this This doc block. Chris@0: */ Chris@0: public function setText($comment) Chris@0: { Chris@0: list(,$short, $long) = $this->splitDocBlock($comment); Chris@0: $this->short_description = $short; Chris@0: $this->long_description = new DocBlock\Description($long, $this); Chris@0: return $this; Chris@0: } Chris@0: /** Chris@0: * Returns the opening line or also known as short description. Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: public function getShortDescription() Chris@0: { Chris@0: return $this->short_description; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the full description or also known as long description. Chris@0: * Chris@0: * @return DocBlock\Description Chris@0: */ Chris@0: public function getLongDescription() Chris@0: { Chris@0: return $this->long_description; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns whether this DocBlock is the start of a Template section. Chris@0: * Chris@0: * A Docblock may serve as template for a series of subsequent DocBlocks. This is indicated by a special marker Chris@0: * (`#@+`) that is appended directly after the opening `/**` of a DocBlock. Chris@0: * Chris@0: * An example of such an opening is: Chris@0: * Chris@0: * ``` Chris@0: * /**#@+ Chris@0: * * My DocBlock Chris@0: * * / Chris@0: * ``` Chris@0: * Chris@0: * The description and tags (not the summary!) are copied onto all subsequent DocBlocks and also applied to all Chris@0: * elements that follow until another DocBlock is found that contains the closing marker (`#@-`). Chris@0: * Chris@0: * @see self::isTemplateEnd() for the check whether a closing marker was provided. Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: public function isTemplateStart() Chris@0: { Chris@0: return $this->isTemplateStart; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns whether this DocBlock is the end of a Template section. Chris@0: * Chris@0: * @see self::isTemplateStart() for a more complete description of the Docblock Template functionality. Chris@0: * Chris@0: * @return boolean Chris@0: */ Chris@0: public function isTemplateEnd() Chris@0: { Chris@0: return $this->isTemplateEnd; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the current context. Chris@0: * Chris@0: * @return Context Chris@0: */ Chris@0: public function getContext() Chris@0: { Chris@0: return $this->context; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the current location. Chris@0: * Chris@0: * @return Location Chris@0: */ Chris@0: public function getLocation() Chris@0: { Chris@0: return $this->location; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the tags for this DocBlock. Chris@0: * Chris@0: * @return Tag[] Chris@0: */ Chris@0: public function getTags() Chris@0: { Chris@0: return $this->tags; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns an array of tags matching the given name. If no tags are found Chris@0: * an empty array is returned. Chris@0: * Chris@0: * @param string $name String to search by. Chris@0: * Chris@0: * @return Tag[] Chris@0: */ Chris@0: public function getTagsByName($name) Chris@0: { Chris@0: $result = array(); Chris@0: Chris@0: /** @var Tag $tag */ Chris@0: foreach ($this->getTags() as $tag) { Chris@0: if ($tag->getName() != $name) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: $result[] = $tag; Chris@0: } Chris@0: Chris@0: return $result; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks if a tag of a certain type is present in this DocBlock. Chris@0: * Chris@0: * @param string $name Tag name to check for. Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: public function hasTag($name) Chris@0: { Chris@0: /** @var Tag $tag */ Chris@0: foreach ($this->getTags() as $tag) { Chris@0: if ($tag->getName() == $name) { Chris@0: return true; Chris@0: } Chris@0: } Chris@0: Chris@0: return false; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Appends a tag at the end of the list of tags. Chris@0: * Chris@0: * @param Tag $tag The tag to add. Chris@0: * Chris@0: * @return Tag The newly added tag. Chris@0: * Chris@0: * @throws \LogicException When the tag belongs to a different DocBlock. Chris@0: */ Chris@0: public function appendTag(Tag $tag) Chris@0: { Chris@0: if (null === $tag->getDocBlock()) { Chris@0: $tag->setDocBlock($this); Chris@0: } Chris@0: Chris@0: if ($tag->getDocBlock() === $this) { Chris@0: $this->tags[] = $tag; Chris@0: } else { Chris@0: throw new \LogicException( Chris@0: 'This tag belongs to a different DocBlock object.' Chris@0: ); Chris@0: } Chris@0: Chris@0: return $tag; Chris@0: } Chris@0: Chris@0: Chris@0: /** Chris@0: * Builds a string representation of this object. Chris@0: * Chris@0: * @todo determine the exact format as used by PHP Reflection and Chris@0: * implement it. Chris@0: * Chris@0: * @return string Chris@0: * @codeCoverageIgnore Not yet implemented Chris@0: */ Chris@0: public static function export() Chris@0: { Chris@0: throw new \Exception('Not yet implemented'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the exported information (we should use the export static method Chris@0: * BUT this throws an exception at this point). Chris@0: * Chris@0: * @return string Chris@0: * @codeCoverageIgnore Not yet implemented Chris@0: */ Chris@0: public function __toString() Chris@0: { Chris@0: return 'Not yet implemented'; Chris@0: } Chris@0: }