123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- <?php
- declare(strict_types=1);
- /**
- * This file is part of phpDocumentor.
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- *
- * @link http://phpdoc.org
- */
- namespace phpDocumentor\Reflection;
- use InvalidArgumentException;
- use LogicException;
- use phpDocumentor\Reflection\DocBlock\DescriptionFactory;
- use phpDocumentor\Reflection\DocBlock\StandardTagFactory;
- use phpDocumentor\Reflection\DocBlock\Tag;
- use phpDocumentor\Reflection\DocBlock\TagFactory;
- use Webmozart\Assert\Assert;
- use function array_shift;
- use function count;
- use function explode;
- use function is_object;
- use function method_exists;
- use function preg_match;
- use function preg_replace;
- use function str_replace;
- use function strpos;
- use function substr;
- use function trim;
- final class DocBlockFactory implements DocBlockFactoryInterface
- {
- /** @var DocBlock\DescriptionFactory */
- private $descriptionFactory;
- /** @var DocBlock\TagFactory */
- private $tagFactory;
- /**
- * Initializes this factory with the required subcontractors.
- */
- public function __construct(DescriptionFactory $descriptionFactory, TagFactory $tagFactory)
- {
- $this->descriptionFactory = $descriptionFactory;
- $this->tagFactory = $tagFactory;
- }
- /**
- * Factory method for easy instantiation.
- *
- * @param array<string, class-string<Tag>> $additionalTags
- */
- public static function createInstance(array $additionalTags = []) : self
- {
- $fqsenResolver = new FqsenResolver();
- $tagFactory = new StandardTagFactory($fqsenResolver);
- $descriptionFactory = new DescriptionFactory($tagFactory);
- $tagFactory->addService($descriptionFactory);
- $tagFactory->addService(new TypeResolver($fqsenResolver));
- $docBlockFactory = new self($descriptionFactory, $tagFactory);
- foreach ($additionalTags as $tagName => $tagHandler) {
- $docBlockFactory->registerTagHandler($tagName, $tagHandler);
- }
- return $docBlockFactory;
- }
- /**
- * @param object|string $docblock A string containing the DocBlock to parse or an object supporting the
- * getDocComment method (such as a ReflectionClass object).
- */
- public function create($docblock, ?Types\Context $context = null, ?Location $location = null) : DocBlock
- {
- if (is_object($docblock)) {
- if (!method_exists($docblock, 'getDocComment')) {
- $exceptionMessage = 'Invalid object passed; the given object must support the getDocComment method';
- throw new InvalidArgumentException($exceptionMessage);
- }
- $docblock = $docblock->getDocComment();
- Assert::string($docblock);
- }
- Assert::stringNotEmpty($docblock);
- if ($context === null) {
- $context = new Types\Context('');
- }
- $parts = $this->splitDocBlock($this->stripDocComment($docblock));
- [$templateMarker, $summary, $description, $tags] = $parts;
- return new DocBlock(
- $summary,
- $description ? $this->descriptionFactory->create($description, $context) : null,
- $this->parseTagBlock($tags, $context),
- $context,
- $location,
- $templateMarker === '#@+',
- $templateMarker === '#@-'
- );
- }
- /**
- * @param class-string<Tag> $handler
- */
- public function registerTagHandler(string $tagName, string $handler) : void
- {
- $this->tagFactory->registerTagHandler($tagName, $handler);
- }
- /**
- * Strips the asterisks from the DocBlock comment.
- *
- * @param string $comment String containing the comment text.
- */
- private function stripDocComment(string $comment) : string
- {
- $comment = preg_replace('#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]?(.*)?#u', '$1', $comment);
- Assert::string($comment);
- $comment = trim($comment);
- // reg ex above is not able to remove */ from a single line docblock
- if (substr($comment, -2) === '*/') {
- $comment = trim(substr($comment, 0, -2));
- }
- return str_replace(["\r\n", "\r"], "\n", $comment);
- }
- // phpcs:disable
- /**
- * Splits the DocBlock into a template marker, summary, description and block of tags.
- *
- * @param string $comment Comment to split into the sub-parts.
- *
- * @return string[] containing the template marker (if any), summary, description and a string containing the tags.
- *
- * @author Mike van Riel <me@mikevanriel.com> for extending the regex with template marker support.
- *
- * @author Richard van Velzen (@_richardJ) Special thanks to Richard for the regex responsible for the split.
- */
- private function splitDocBlock(string $comment) : array
- {
- // phpcs:enable
- // 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 ['', '', '', $comment];
- }
- // clears all extra horizontal whitespace from the line endings to prevent parsing issues
- $comment = preg_replace('/\h*$/Sum', '', $comment);
- Assert::string($comment);
- /*
- * Splits the docblock into a template marker, summary, 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.
- * @param Types\Context $context Context of the parsed Tag
- *
- * @return DocBlock\Tag[]
- */
- private function parseTagBlock(string $tags, Types\Context $context) : array
- {
- $tags = $this->filterTagBlock($tags);
- if ($tags === null) {
- return [];
- }
- $result = [];
- $lines = $this->splitTagBlockIntoTagLines($tags);
- foreach ($lines as $key => $tagLine) {
- $result[$key] = $this->tagFactory->create(trim($tagLine), $context);
- }
- return $result;
- }
- /**
- * @return string[]
- */
- private function splitTagBlockIntoTagLines(string $tags) : array
- {
- $result = [];
- foreach (explode("\n", $tags) as $tagLine) {
- if ($tagLine !== '' && strpos($tagLine, '@') === 0) {
- $result[] = $tagLine;
- } else {
- $result[count($result) - 1] .= "\n" . $tagLine;
- }
- }
- return $result;
- }
- private function filterTagBlock(string $tags) : ?string
- {
- $tags = trim($tags);
- if (!$tags) {
- return null;
- }
- if ($tags[0] !== '@') {
- // @codeCoverageIgnoreStart
- // Can't simulate this; this only happens if there is an error with the parsing of the DocBlock that
- // we didn't foresee.
- throw new LogicException('A tag block started with text instead of an at-sign(@): ' . $tags);
- // @codeCoverageIgnoreEnd
- }
- return $tags;
- }
- }
|