Chris@12: Chris@12: * @license http://www.opensource.org/licenses/mit-license.php MIT Chris@12: * @link http://phpdoc.org Chris@12: */ Chris@12: Chris@12: namespace phpDocumentor\Reflection; Chris@12: Chris@12: use phpDocumentor\Reflection\DocBlock\DescriptionFactory; Chris@12: use phpDocumentor\Reflection\DocBlock\StandardTagFactory; Chris@12: use phpDocumentor\Reflection\DocBlock\Tag; Chris@12: use phpDocumentor\Reflection\DocBlock\TagFactory; Chris@12: use Webmozart\Assert\Assert; Chris@12: Chris@12: final class DocBlockFactory implements DocBlockFactoryInterface Chris@12: { Chris@12: /** @var DocBlock\DescriptionFactory */ Chris@12: private $descriptionFactory; Chris@12: Chris@12: /** @var DocBlock\TagFactory */ Chris@12: private $tagFactory; Chris@12: Chris@12: /** Chris@12: * Initializes this factory with the required subcontractors. Chris@12: * Chris@12: * @param DescriptionFactory $descriptionFactory Chris@12: * @param TagFactory $tagFactory Chris@12: */ Chris@12: public function __construct(DescriptionFactory $descriptionFactory, TagFactory $tagFactory) Chris@12: { Chris@12: $this->descriptionFactory = $descriptionFactory; Chris@12: $this->tagFactory = $tagFactory; Chris@12: } Chris@12: Chris@12: /** Chris@12: * Factory method for easy instantiation. Chris@12: * Chris@12: * @param string[] $additionalTags Chris@12: * Chris@12: * @return DocBlockFactory Chris@12: */ Chris@12: public static function createInstance(array $additionalTags = []) Chris@12: { Chris@12: $fqsenResolver = new FqsenResolver(); Chris@12: $tagFactory = new StandardTagFactory($fqsenResolver); Chris@12: $descriptionFactory = new DescriptionFactory($tagFactory); Chris@12: Chris@12: $tagFactory->addService($descriptionFactory); Chris@12: $tagFactory->addService(new TypeResolver($fqsenResolver)); Chris@12: Chris@12: $docBlockFactory = new self($descriptionFactory, $tagFactory); Chris@12: foreach ($additionalTags as $tagName => $tagHandler) { Chris@12: $docBlockFactory->registerTagHandler($tagName, $tagHandler); Chris@12: } Chris@12: Chris@12: return $docBlockFactory; Chris@12: } Chris@12: Chris@12: /** Chris@12: * @param object|string $docblock A string containing the DocBlock to parse or an object supporting the Chris@12: * getDocComment method (such as a ReflectionClass object). Chris@12: * @param Types\Context $context Chris@12: * @param Location $location Chris@12: * Chris@12: * @return DocBlock Chris@12: */ Chris@12: public function create($docblock, Types\Context $context = null, Location $location = null) Chris@12: { Chris@12: if (is_object($docblock)) { Chris@12: if (!method_exists($docblock, 'getDocComment')) { Chris@12: $exceptionMessage = 'Invalid object passed; the given object must support the getDocComment method'; Chris@12: throw new \InvalidArgumentException($exceptionMessage); Chris@12: } Chris@12: Chris@12: $docblock = $docblock->getDocComment(); Chris@12: } Chris@12: Chris@12: Assert::stringNotEmpty($docblock); Chris@12: Chris@12: if ($context === null) { Chris@12: $context = new Types\Context(''); Chris@12: } Chris@12: Chris@12: $parts = $this->splitDocBlock($this->stripDocComment($docblock)); Chris@12: list($templateMarker, $summary, $description, $tags) = $parts; Chris@12: Chris@12: return new DocBlock( Chris@12: $summary, Chris@12: $description ? $this->descriptionFactory->create($description, $context) : null, Chris@12: array_filter($this->parseTagBlock($tags, $context), function ($tag) { Chris@12: return $tag instanceof Tag; Chris@12: }), Chris@12: $context, Chris@12: $location, Chris@12: $templateMarker === '#@+', Chris@12: $templateMarker === '#@-' Chris@12: ); Chris@12: } Chris@12: Chris@12: public function registerTagHandler($tagName, $handler) Chris@12: { Chris@12: $this->tagFactory->registerTagHandler($tagName, $handler); Chris@12: } Chris@12: Chris@12: /** Chris@12: * Strips the asterisks from the DocBlock comment. Chris@12: * Chris@12: * @param string $comment String containing the comment text. Chris@12: * Chris@12: * @return string Chris@12: */ Chris@12: private function stripDocComment($comment) Chris@12: { Chris@12: $comment = trim(preg_replace('#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]{0,1}(.*)?#u', '$1', $comment)); Chris@12: Chris@12: // reg ex above is not able to remove */ from a single line docblock Chris@12: if (substr($comment, -2) === '*/') { Chris@12: $comment = trim(substr($comment, 0, -2)); Chris@12: } Chris@12: Chris@12: return str_replace(["\r\n", "\r"], "\n", $comment); Chris@12: } Chris@12: Chris@12: /** Chris@12: * Splits the DocBlock into a template marker, summary, description and block of tags. Chris@12: * Chris@12: * @param string $comment Comment to split into the sub-parts. Chris@12: * Chris@12: * @author Richard van Velzen (@_richardJ) Special thanks to Richard for the regex responsible for the split. Chris@12: * @author Mike van Riel for extending the regex with template marker support. Chris@12: * Chris@12: * @return string[] containing the template marker (if any), summary, description and a string containing the tags. Chris@12: */ Chris@12: private function splitDocBlock($comment) Chris@12: { Chris@12: // Performance improvement cheat: if the first character is an @ then only tags are in this DocBlock. This Chris@12: // method does not split tags so we return this verbatim as the fourth result (tags). This saves us the Chris@12: // performance impact of running a regular expression Chris@12: if (strpos($comment, '@') === 0) { Chris@12: return ['', '', '', $comment]; Chris@12: } Chris@12: Chris@12: // clears all extra horizontal whitespace from the line endings to prevent parsing issues Chris@12: $comment = preg_replace('/\h*$/Sum', '', $comment); Chris@12: Chris@12: /* Chris@12: * Splits the docblock into a template marker, summary, description and tags section. Chris@12: * Chris@12: * - The template marker is empty, #@+ or #@- if the DocBlock starts with either of those (a newline may Chris@12: * occur after it and will be stripped). Chris@12: * - The short description is started from the first character until a dot is encountered followed by a Chris@12: * newline OR two consecutive newlines (horizontal whitespace is taken into account to consider spacing Chris@12: * errors). This is optional. Chris@12: * - The long description, any character until a new line is encountered followed by an @ and word Chris@12: * characters (a tag). This is optional. Chris@12: * - Tags; the remaining characters Chris@12: * Chris@12: * Big thanks to RichardJ for contributing this Regular Expression Chris@12: */ Chris@12: preg_match( Chris@12: '/ Chris@12: \A Chris@12: # 1. Extract the template marker Chris@12: (?:(\#\@\+|\#\@\-)\n?)? Chris@12: Chris@12: # 2. Extract the summary Chris@12: (?: Chris@12: (?! @\pL ) # The summary may not start with an @ Chris@12: ( Chris@12: [^\n.]+ Chris@12: (?: Chris@12: (?! \. \n | \n{2} ) # End summary upon a dot followed by newline or two newlines Chris@12: [\n.] (?! [ \t]* @\pL ) # End summary when an @ is found as first character on a new line Chris@12: [^\n.]+ # Include anything else Chris@12: )* Chris@12: \.? Chris@12: )? Chris@12: ) Chris@12: Chris@12: # 3. Extract the description Chris@12: (?: Chris@12: \s* # Some form of whitespace _must_ precede a description because a summary must be there Chris@12: (?! @\pL ) # The description may not start with an @ Chris@12: ( Chris@12: [^\n]+ Chris@12: (?: \n+ Chris@12: (?! [ \t]* @\pL ) # End description when an @ is found as first character on a new line Chris@12: [^\n]+ # Include anything else Chris@12: )* Chris@12: ) Chris@12: )? Chris@12: Chris@12: # 4. Extract the tags (anything that follows) Chris@12: (\s+ [\s\S]*)? # everything that follows Chris@12: /ux', Chris@12: $comment, Chris@12: $matches Chris@12: ); Chris@12: array_shift($matches); Chris@12: Chris@12: while (count($matches) < 4) { Chris@12: $matches[] = ''; Chris@12: } Chris@12: Chris@12: return $matches; Chris@12: } Chris@12: Chris@12: /** Chris@12: * Creates the tag objects. Chris@12: * Chris@12: * @param string $tags Tag block to parse. Chris@12: * @param Types\Context $context Context of the parsed Tag Chris@12: * Chris@12: * @return DocBlock\Tag[] Chris@12: */ Chris@12: private function parseTagBlock($tags, Types\Context $context) Chris@12: { Chris@12: $tags = $this->filterTagBlock($tags); Chris@12: if (!$tags) { Chris@12: return []; Chris@12: } Chris@12: Chris@12: $result = $this->splitTagBlockIntoTagLines($tags); Chris@12: foreach ($result as $key => $tagLine) { Chris@12: $result[$key] = $this->tagFactory->create(trim($tagLine), $context); Chris@12: } Chris@12: Chris@12: return $result; Chris@12: } Chris@12: Chris@12: /** Chris@12: * @param string $tags Chris@12: * Chris@12: * @return string[] Chris@12: */ Chris@12: private function splitTagBlockIntoTagLines($tags) Chris@12: { Chris@12: $result = []; Chris@12: foreach (explode("\n", $tags) as $tag_line) { Chris@12: if (isset($tag_line[0]) && ($tag_line[0] === '@')) { Chris@12: $result[] = $tag_line; Chris@12: } else { Chris@12: $result[count($result) - 1] .= "\n" . $tag_line; Chris@12: } Chris@12: } Chris@12: Chris@12: return $result; Chris@12: } Chris@12: Chris@12: /** Chris@12: * @param $tags Chris@12: * @return string Chris@12: */ Chris@12: private function filterTagBlock($tags) Chris@12: { Chris@12: $tags = trim($tags); Chris@12: if (!$tags) { Chris@12: return null; Chris@12: } Chris@12: Chris@12: if ('@' !== $tags[0]) { Chris@12: // @codeCoverageIgnoreStart Chris@12: // Can't simulate this; this only happens if there is an error with the parsing of the DocBlock that Chris@12: // we didn't foresee. Chris@12: throw new \LogicException('A tag block started with text instead of an at-sign(@): ' . $tags); Chris@12: // @codeCoverageIgnoreEnd Chris@12: } Chris@12: Chris@12: return $tags; Chris@12: } Chris@12: }