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