├── LICENSE ├── README.md ├── composer.json └── src ├── DocBlock.php ├── DocBlock ├── Description.php ├── DescriptionFactory.php ├── ExampleFinder.php ├── Serializer.php ├── StandardTagFactory.php ├── Tag.php ├── TagFactory.php └── Tags │ ├── Author.php │ ├── BaseTag.php │ ├── Covers.php │ ├── Deprecated.php │ ├── Example.php │ ├── Extends_.php │ ├── Factory │ ├── AbstractPHPStanFactory.php │ ├── ExtendsFactory.php │ ├── Factory.php │ ├── ImplementsFactory.php │ ├── MethodFactory.php │ ├── MethodParameterFactory.php │ ├── PHPStanFactory.php │ ├── ParamFactory.php │ ├── PropertyFactory.php │ ├── PropertyReadFactory.php │ ├── PropertyWriteFactory.php │ ├── ReturnFactory.php │ ├── StaticMethod.php │ ├── TemplateExtendsFactory.php │ ├── TemplateFactory.php │ ├── TemplateImplementsFactory.php │ └── VarFactory.php │ ├── Formatter.php │ ├── Formatter │ ├── AlignFormatter.php │ └── PassthroughFormatter.php │ ├── Generic.php │ ├── Implements_.php │ ├── InvalidTag.php │ ├── Link.php │ ├── Method.php │ ├── MethodParameter.php │ ├── Mixin.php │ ├── Param.php │ ├── Property.php │ ├── PropertyRead.php │ ├── PropertyWrite.php │ ├── Reference │ ├── Fqsen.php │ ├── Reference.php │ └── Url.php │ ├── Return_.php │ ├── See.php │ ├── Since.php │ ├── Source.php │ ├── TagWithType.php │ ├── Template.php │ ├── TemplateCovariant.php │ ├── TemplateExtends.php │ ├── TemplateImplements.php │ ├── Throws.php │ ├── Uses.php │ ├── Var_.php │ └── Version.php ├── DocBlockFactory.php ├── DocBlockFactoryInterface.php ├── Exception └── PcreException.php └── Utils.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2010 Mike van Riel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 2 | [![Integrate](https://github.com/phpDocumentor/ReflectionDocBlock/actions/workflows/integrate.yaml/badge.svg)](https://github.com/phpDocumentor/ReflectionDocBlock/actions/workflows/integrate.yaml) 3 | [![Scrutinizer Code Coverage](https://img.shields.io/scrutinizer/coverage/g/phpDocumentor/ReflectionDocBlock.svg)](https://scrutinizer-ci.com/g/phpDocumentor/ReflectionDocBlock/?branch=master) 4 | [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/phpDocumentor/ReflectionDocBlock.svg)](https://scrutinizer-ci.com/g/phpDocumentor/ReflectionDocBlock/?branch=master) 5 | [![Stable Version](https://img.shields.io/packagist/v/phpdocumentor/reflection-docblock.svg?label=stable)](https://packagist.org/packages/phpdocumentor/reflection-docblock) 6 | [![Unstable Version](https://img.shields.io/packagist/v/phpdocumentor/reflection-docblock.svg?include_prereleases&label=unstable)](https://packagist.org/packages/phpdocumentor/reflection-docblock) 7 | 8 | ReflectionDocBlock 9 | ================== 10 | 11 | Introduction 12 | ------------ 13 | 14 | The ReflectionDocBlock component of phpDocumentor provides a DocBlock parser 15 | that is 100% compatible with the [PHPDoc standard](http://phpdoc.org/docs/latest). 16 | 17 | With this component, a library can provide support for annotations via DocBlocks 18 | or otherwise retrieve information that is embedded in a DocBlock. 19 | 20 | Installation 21 | ------------ 22 | 23 | ```bash 24 | composer require phpdocumentor/reflection-docblock 25 | ``` 26 | 27 | Usage 28 | ----- 29 | 30 | In order to parse the DocBlock one needs a DocBlockFactory that can be 31 | instantiated using its `createInstance` factory method like this: 32 | 33 | ```php 34 | $factory = \phpDocumentor\Reflection\DocBlockFactory::createInstance(); 35 | ``` 36 | 37 | Then we can use the `create` method of the factory to interpret the DocBlock. 38 | Please note that it is also possible to provide a class that has the 39 | `getDocComment()` method, such as an object of type `ReflectionClass`, the 40 | create method will read that if it exists. 41 | 42 | ```php 43 | $docComment = <<create($docComment); 55 | ``` 56 | 57 | The `create` method will yield an object of type `\phpDocumentor\Reflection\DocBlock` 58 | whose methods can be queried: 59 | 60 | ```php 61 | // Contains the summary for this DocBlock 62 | $summary = $docblock->getSummary(); 63 | 64 | // Contains \phpDocumentor\Reflection\DocBlock\Description object 65 | $description = $docblock->getDescription(); 66 | 67 | // You can either cast it to string 68 | $description = (string) $docblock->getDescription(); 69 | 70 | // Or use the render method to get a string representation of the Description. 71 | $description = $docblock->getDescription()->render(); 72 | ``` 73 | 74 | > For more examples it would be best to review the scripts in the [`/examples` folder](/examples). 75 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpdocumentor/reflection-docblock", 3 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Mike van Riel", 9 | "email": "me@mikevanriel.com" 10 | }, 11 | { 12 | "name": "Jaap van Otterdijk", 13 | "email": "opensource@ijaap.nl" 14 | } 15 | ], 16 | "require": { 17 | "php": "^7.4 || ^8.0", 18 | "phpdocumentor/type-resolver": "^1.7", 19 | "webmozart/assert": "^1.9.1", 20 | "phpdocumentor/reflection-common": "^2.2", 21 | "ext-filter": "*", 22 | "phpstan/phpdoc-parser": "^1.7|^2.0", 23 | "doctrine/deprecations": "^1.1" 24 | }, 25 | "require-dev": { 26 | "mockery/mockery": "~1.3.5 || ~1.6.0", 27 | "phpunit/phpunit": "^9.5", 28 | "phpstan/phpstan": "^1.8", 29 | "phpstan/phpstan-mockery": "^1.1", 30 | "phpstan/extension-installer": "^1.1", 31 | "phpstan/phpstan-webmozart-assert": "^1.2", 32 | "psalm/phar": "^5.26" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "phpDocumentor\\Reflection\\": "src" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "phpDocumentor\\Reflection\\": ["tests/unit", "tests/integration"] 42 | } 43 | }, 44 | "config": { 45 | "platform": { 46 | "php":"7.4.0" 47 | }, 48 | "allow-plugins": { 49 | "phpstan/extension-installer": true 50 | } 51 | }, 52 | "extra": { 53 | "branch-alias": { 54 | "dev-master": "5.x-dev" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/DocBlock.php: -------------------------------------------------------------------------------- 1 | summary = $summary; 60 | $this->description = $description ?: new DocBlock\Description(''); 61 | foreach ($tags as $tag) { 62 | $this->addTag($tag); 63 | } 64 | 65 | $this->context = $context; 66 | $this->location = $location; 67 | 68 | $this->isTemplateEnd = $isTemplateEnd; 69 | $this->isTemplateStart = $isTemplateStart; 70 | } 71 | 72 | public function getSummary(): string 73 | { 74 | return $this->summary; 75 | } 76 | 77 | public function getDescription(): DocBlock\Description 78 | { 79 | return $this->description; 80 | } 81 | 82 | /** 83 | * Returns the current context. 84 | */ 85 | public function getContext(): ?Types\Context 86 | { 87 | return $this->context; 88 | } 89 | 90 | /** 91 | * Returns the current location. 92 | */ 93 | public function getLocation(): ?Location 94 | { 95 | return $this->location; 96 | } 97 | 98 | /** 99 | * Returns whether this DocBlock is the start of a Template section. 100 | * 101 | * A Docblock may serve as template for a series of subsequent DocBlocks. This is indicated by a special marker 102 | * (`#@+`) that is appended directly after the opening `/**` of a DocBlock. 103 | * 104 | * An example of such an opening is: 105 | * 106 | * ``` 107 | * /**#@+ 108 | * * My DocBlock 109 | * * / 110 | * ``` 111 | * 112 | * The description and tags (not the summary!) are copied onto all subsequent DocBlocks and also applied to all 113 | * elements that follow until another DocBlock is found that contains the closing marker (`#@-`). 114 | * 115 | * @see self::isTemplateEnd() for the check whether a closing marker was provided. 116 | */ 117 | public function isTemplateStart(): bool 118 | { 119 | return $this->isTemplateStart; 120 | } 121 | 122 | /** 123 | * Returns whether this DocBlock is the end of a Template section. 124 | * 125 | * @see self::isTemplateStart() for a more complete description of the Docblock Template functionality. 126 | */ 127 | public function isTemplateEnd(): bool 128 | { 129 | return $this->isTemplateEnd; 130 | } 131 | 132 | /** 133 | * Returns the tags for this DocBlock. 134 | * 135 | * @return Tag[] 136 | */ 137 | public function getTags(): array 138 | { 139 | return $this->tags; 140 | } 141 | 142 | /** 143 | * Returns an array of tags matching the given name. If no tags are found 144 | * an empty array is returned. 145 | * 146 | * @param string $name String to search by. 147 | * 148 | * @return Tag[] 149 | */ 150 | public function getTagsByName(string $name): array 151 | { 152 | $result = []; 153 | 154 | foreach ($this->getTags() as $tag) { 155 | if ($tag->getName() !== $name) { 156 | continue; 157 | } 158 | 159 | $result[] = $tag; 160 | } 161 | 162 | return $result; 163 | } 164 | 165 | /** 166 | * Returns an array of tags with type matching the given name. If no tags are found 167 | * an empty array is returned. 168 | * 169 | * @param string $name String to search by. 170 | * 171 | * @return TagWithType[] 172 | */ 173 | public function getTagsWithTypeByName(string $name): array 174 | { 175 | $result = []; 176 | 177 | foreach ($this->getTagsByName($name) as $tag) { 178 | if (!$tag instanceof TagWithType) { 179 | continue; 180 | } 181 | 182 | $result[] = $tag; 183 | } 184 | 185 | return $result; 186 | } 187 | 188 | /** 189 | * Checks if a tag of a certain type is present in this DocBlock. 190 | * 191 | * @param string $name Tag name to check for. 192 | */ 193 | public function hasTag(string $name): bool 194 | { 195 | foreach ($this->getTags() as $tag) { 196 | if ($tag->getName() === $name) { 197 | return true; 198 | } 199 | } 200 | 201 | return false; 202 | } 203 | 204 | /** 205 | * Remove a tag from this DocBlock. 206 | * 207 | * @param Tag $tagToRemove The tag to remove. 208 | */ 209 | public function removeTag(Tag $tagToRemove): void 210 | { 211 | foreach ($this->tags as $key => $tag) { 212 | if ($tag === $tagToRemove) { 213 | unset($this->tags[$key]); 214 | break; 215 | } 216 | } 217 | } 218 | 219 | /** 220 | * Adds a tag to this DocBlock. 221 | * 222 | * @param Tag $tag The tag to add. 223 | */ 224 | private function addTag(Tag $tag): void 225 | { 226 | $this->tags[] = $tag; 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/DocBlock/Description.php: -------------------------------------------------------------------------------- 1 | create('This is a {@see Description}', $context); 32 | * 33 | * The description factory will interpret the given body and create a body template and list of tags from them, and pass 34 | * that onto the constructor if this class. 35 | * 36 | * > The $context variable is a class of type {@see \phpDocumentor\Reflection\Types\Context} and contains the namespace 37 | * > and the namespace aliases that apply to this DocBlock. These are used by the Factory to resolve and expand partial 38 | * > type names and FQSENs. 39 | * 40 | * If you do not want to use the DescriptionFactory you can pass a body template and tag listing like this: 41 | * 42 | * $description = new Description( 43 | * 'This is a %1$s', 44 | * [ new See(new Fqsen('\phpDocumentor\Reflection\DocBlock\Description')) ] 45 | * ); 46 | * 47 | * It is generally recommended to use the Factory as that will also apply escaping rules, while the Description object 48 | * is mainly responsible for rendering. 49 | * 50 | * @see DescriptionFactory to create a new Description. 51 | * @see Tags\Formatter for the formatting of the body and tags. 52 | */ 53 | class Description 54 | { 55 | private string $bodyTemplate; 56 | 57 | /** @var Tag[] */ 58 | private array $tags; 59 | 60 | /** 61 | * Initializes a Description with its body (template) and a listing of the tags used in the body template. 62 | * 63 | * @param Tag[] $tags 64 | */ 65 | public function __construct(string $bodyTemplate, array $tags = []) 66 | { 67 | $this->bodyTemplate = $bodyTemplate; 68 | $this->tags = $tags; 69 | } 70 | 71 | /** 72 | * Returns the body template. 73 | */ 74 | public function getBodyTemplate(): string 75 | { 76 | return $this->bodyTemplate; 77 | } 78 | 79 | /** 80 | * Returns the tags for this DocBlock. 81 | * 82 | * @return Tag[] 83 | */ 84 | public function getTags(): array 85 | { 86 | return $this->tags; 87 | } 88 | 89 | /** 90 | * Renders this description as a string where the provided formatter will format the tags in the expected string 91 | * format. 92 | */ 93 | public function render(?Formatter $formatter = null): string 94 | { 95 | if ($this->tags === []) { 96 | return vsprintf($this->bodyTemplate, []); 97 | } 98 | 99 | if ($formatter === null) { 100 | $formatter = new PassthroughFormatter(); 101 | } 102 | 103 | $tags = []; 104 | foreach ($this->tags as $tag) { 105 | $tags[] = '{' . $formatter->format($tag) . '}'; 106 | } 107 | 108 | return vsprintf($this->bodyTemplate, $tags); 109 | } 110 | 111 | /** 112 | * Returns a plain string representation of this description. 113 | */ 114 | public function __toString(): string 115 | { 116 | return $this->render(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/DocBlock/DescriptionFactory.php: -------------------------------------------------------------------------------- 1 | tagFactory = $tagFactory; 59 | } 60 | 61 | /** 62 | * Returns the parsed text of this description. 63 | */ 64 | public function create(string $contents, ?TypeContext $context = null): Description 65 | { 66 | $tokens = $this->lex($contents); 67 | $count = count($tokens); 68 | $tagCount = 0; 69 | $tags = []; 70 | 71 | for ($i = 1; $i < $count; $i += 2) { 72 | $tags[] = $this->tagFactory->create($tokens[$i], $context); 73 | $tokens[$i] = '%' . ++$tagCount . '$s'; 74 | } 75 | 76 | //In order to allow "literal" inline tags, the otherwise invalid 77 | //sequence "{@}" is changed to "@", and "{}" is changed to "}". 78 | //"%" is escaped to "%%" because of vsprintf. 79 | //See unit tests for examples. 80 | for ($i = 0; $i < $count; $i += 2) { 81 | $tokens[$i] = str_replace(['{@}', '{}', '%'], ['@', '}', '%%'], $tokens[$i]); 82 | } 83 | 84 | return new Description(implode('', $tokens), $tags); 85 | } 86 | 87 | /** 88 | * Strips the contents from superfluous whitespace and splits the description into a series of tokens. 89 | * 90 | * @return string[] A series of tokens of which the description text is composed. 91 | */ 92 | private function lex(string $contents): array 93 | { 94 | $contents = $this->removeSuperfluousStartingWhitespace($contents); 95 | 96 | // performance optimalization; if there is no inline tag, don't bother splitting it up. 97 | if (strpos($contents, '{@') === false) { 98 | return [$contents]; 99 | } 100 | 101 | return Utils::pregSplit( 102 | '/\{ 103 | # "{@}" is not a valid inline tag. This ensures that we do not treat it as one, but treat it literally. 104 | (?!@\}) 105 | # We want to capture the whole tag line, but without the inline tag delimiters. 106 | (\@ 107 | # Match everything up to the next delimiter. 108 | [^{}]* 109 | # Nested inline tag content should not be captured, or it will appear in the result separately. 110 | (?: 111 | # Match nested inline tags. 112 | (?: 113 | # Because we did not catch the tag delimiters earlier, we must be explicit with them here. 114 | # Notice that this also matches "{}", as a way to later introduce it as an escape sequence. 115 | \{(?1)?\} 116 | | 117 | # Make sure we match hanging "{". 118 | \{ 119 | ) 120 | # Match content after the nested inline tag. 121 | [^{}]* 122 | )* # If there are more inline tags, match them as well. We use "*" since there may not be any 123 | # nested inline tags. 124 | ) 125 | \}/Sux', 126 | $contents, 127 | 0, 128 | PREG_SPLIT_DELIM_CAPTURE 129 | ); 130 | } 131 | 132 | /** 133 | * Removes the superfluous from a multi-line description. 134 | * 135 | * When a description has more than one line then it can happen that the second and subsequent lines have an 136 | * additional indentation. This is commonly in use with tags like this: 137 | * 138 | * {@}since 1.1.0 This is an example 139 | * description where we have an 140 | * indentation in the second and 141 | * subsequent lines. 142 | * 143 | * If we do not normalize the indentation then we have superfluous whitespace on the second and subsequent 144 | * lines and this may cause rendering issues when, for example, using a Markdown converter. 145 | */ 146 | private function removeSuperfluousStartingWhitespace(string $contents): string 147 | { 148 | $lines = Utils::pregSplit("/\r\n?|\n/", $contents); 149 | 150 | // if there is only one line then we don't have lines with superfluous whitespace and 151 | // can use the contents as-is 152 | if (count($lines) <= 1) { 153 | return $contents; 154 | } 155 | 156 | // determine how many whitespace characters need to be stripped 157 | $startingSpaceCount = 9999999; 158 | for ($i = 1, $iMax = count($lines); $i < $iMax; ++$i) { 159 | // lines with a no length do not count as they are not indented at all 160 | if (trim($lines[$i]) === '') { 161 | continue; 162 | } 163 | 164 | // determine the number of prefixing spaces by checking the difference in line length before and after 165 | // an ltrim 166 | $startingSpaceCount = min($startingSpaceCount, strlen($lines[$i]) - strlen(ltrim($lines[$i]))); 167 | } 168 | 169 | // strip the number of spaces from each line 170 | if ($startingSpaceCount > 0) { 171 | for ($i = 1, $iMax = count($lines); $i < $iMax; ++$i) { 172 | $lines[$i] = substr($lines[$i], $startingSpaceCount); 173 | } 174 | } 175 | 176 | return implode("\n", $lines); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/DocBlock/ExampleFinder.php: -------------------------------------------------------------------------------- 1 | getFilePath(); 45 | 46 | $file = $this->getExampleFileContents($filename); 47 | if ($file === null) { 48 | return sprintf('** File not found : %s **', $filename); 49 | } 50 | 51 | return implode('', array_slice($file, $example->getStartingLine() - 1, $example->getLineCount())); 52 | } 53 | 54 | /** 55 | * Registers the project's root directory where an 'examples' folder can be expected. 56 | */ 57 | public function setSourceDirectory(string $directory = ''): void 58 | { 59 | $this->sourceDirectory = $directory; 60 | } 61 | 62 | /** 63 | * Returns the project's root directory where an 'examples' folder can be expected. 64 | */ 65 | public function getSourceDirectory(): string 66 | { 67 | return $this->sourceDirectory; 68 | } 69 | 70 | /** 71 | * Registers a series of directories that may contain examples. 72 | * 73 | * @param string[] $directories 74 | */ 75 | public function setExampleDirectories(array $directories): void 76 | { 77 | $this->exampleDirectories = $directories; 78 | } 79 | 80 | /** 81 | * Returns a series of directories that may contain examples. 82 | * 83 | * @return string[] 84 | */ 85 | public function getExampleDirectories(): array 86 | { 87 | return $this->exampleDirectories; 88 | } 89 | 90 | /** 91 | * Attempts to find the requested example file and returns its contents or null if no file was found. 92 | * 93 | * This method will try several methods in search of the given example file, the first one it encounters is 94 | * returned: 95 | * 96 | * 1. Iterates through all examples folders for the given filename 97 | * 2. Checks the source folder for the given filename 98 | * 3. Checks the 'examples' folder in the current working directory for examples 99 | * 4. Checks the path relative to the current working directory for the given filename 100 | * 101 | * @return string[] all lines of the example file 102 | */ 103 | private function getExampleFileContents(string $filename): ?array 104 | { 105 | $normalizedPath = null; 106 | 107 | foreach ($this->exampleDirectories as $directory) { 108 | $exampleFileFromConfig = $this->constructExamplePath($directory, $filename); 109 | if (is_readable($exampleFileFromConfig)) { 110 | $normalizedPath = $exampleFileFromConfig; 111 | break; 112 | } 113 | } 114 | 115 | if ($normalizedPath === null) { 116 | if (is_readable($this->getExamplePathFromSource($filename))) { 117 | $normalizedPath = $this->getExamplePathFromSource($filename); 118 | } elseif (is_readable($this->getExamplePathFromExampleDirectory($filename))) { 119 | $normalizedPath = $this->getExamplePathFromExampleDirectory($filename); 120 | } elseif (is_readable($filename)) { 121 | $normalizedPath = $filename; 122 | } 123 | } 124 | 125 | $lines = $normalizedPath !== null && is_readable($normalizedPath) ? file($normalizedPath) : false; 126 | 127 | return $lines !== false ? $lines : null; 128 | } 129 | 130 | /** 131 | * Get example filepath based on the example directory inside your project. 132 | */ 133 | private function getExamplePathFromExampleDirectory(string $file): string 134 | { 135 | return getcwd() . DIRECTORY_SEPARATOR . 'examples' . DIRECTORY_SEPARATOR . $file; 136 | } 137 | 138 | /** 139 | * Returns a path to the example file in the given directory.. 140 | */ 141 | private function constructExamplePath(string $directory, string $file): string 142 | { 143 | return rtrim($directory, '\\/') . DIRECTORY_SEPARATOR . $file; 144 | } 145 | 146 | /** 147 | * Get example filepath based on sourcecode. 148 | */ 149 | private function getExamplePathFromSource(string $file): string 150 | { 151 | return sprintf( 152 | '%s%s%s', 153 | trim($this->getSourceDirectory(), '\\/'), 154 | DIRECTORY_SEPARATOR, 155 | trim($file, '"') 156 | ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/DocBlock/Serializer.php: -------------------------------------------------------------------------------- 1 | indent = $indent; 66 | $this->indentString = $indentString; 67 | $this->isFirstLineIndented = $indentFirstLine; 68 | $this->lineLength = $lineLength; 69 | $this->tagFormatter = $tagFormatter ?: new PassthroughFormatter(); 70 | $this->lineEnding = $lineEnding; 71 | } 72 | 73 | /** 74 | * Generate a DocBlock comment. 75 | * 76 | * @param DocBlock $docblock The DocBlock to serialize. 77 | * 78 | * @return string The serialized doc block. 79 | */ 80 | public function getDocComment(DocBlock $docblock): string 81 | { 82 | $indent = str_repeat($this->indentString, $this->indent); 83 | $firstIndent = $this->isFirstLineIndented ? $indent : ''; 84 | // 3 === strlen(' * ') 85 | $wrapLength = $this->lineLength !== null ? $this->lineLength - strlen($indent) - 3 : null; 86 | 87 | $text = $this->removeTrailingSpaces( 88 | $indent, 89 | $this->addAsterisksForEachLine( 90 | $indent, 91 | $this->getSummaryAndDescriptionTextBlock($docblock, $wrapLength) 92 | ) 93 | ); 94 | 95 | $comment = $firstIndent . "/**\n"; 96 | if ($text) { 97 | $comment .= $indent . ' * ' . $text . "\n"; 98 | $comment .= $indent . " *\n"; 99 | } 100 | 101 | $comment = $this->addTagBlock($docblock, $wrapLength, $indent, $comment); 102 | 103 | return str_replace("\n", $this->lineEnding, $comment . $indent . ' */'); 104 | } 105 | 106 | private function removeTrailingSpaces(string $indent, string $text): string 107 | { 108 | return str_replace( 109 | sprintf("\n%s * \n", $indent), 110 | sprintf("\n%s *\n", $indent), 111 | $text 112 | ); 113 | } 114 | 115 | private function addAsterisksForEachLine(string $indent, string $text): string 116 | { 117 | return str_replace( 118 | "\n", 119 | sprintf("\n%s * ", $indent), 120 | $text 121 | ); 122 | } 123 | 124 | private function getSummaryAndDescriptionTextBlock(DocBlock $docblock, ?int $wrapLength): string 125 | { 126 | $text = $docblock->getSummary() . ((string) $docblock->getDescription() ? "\n\n" . $docblock->getDescription() 127 | : ''); 128 | if ($wrapLength !== null) { 129 | $text = wordwrap($text, $wrapLength); 130 | 131 | return $text; 132 | } 133 | 134 | return $text; 135 | } 136 | 137 | private function addTagBlock(DocBlock $docblock, ?int $wrapLength, string $indent, string $comment): string 138 | { 139 | foreach ($docblock->getTags() as $tag) { 140 | $tagText = $this->tagFormatter->format($tag); 141 | if ($wrapLength !== null) { 142 | $tagText = wordwrap($tagText, $wrapLength); 143 | } 144 | 145 | $tagText = str_replace( 146 | "\n", 147 | sprintf("\n%s * ", $indent), 148 | $tagText 149 | ); 150 | 151 | $comment .= sprintf("%s * %s\n", $indent, $tagText); 152 | } 153 | 154 | return $comment; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/DocBlock/StandardTagFactory.php: -------------------------------------------------------------------------------- 1 | Important: each parameter in addition to the body variable for the `create` method must default to null, otherwise 65 | * > it violates the constraint with the interface; it is recommended to use the {@see Assert::notNull()} method to 66 | * > verify that a dependency is actually passed. 67 | * 68 | * This Factory also features a Service Locator component that is used to pass the right dependencies to the 69 | * `create` method of a tag; each dependency should be registered as a service or as a parameter. 70 | * 71 | * When you want to use a Tag of your own with custom handling you need to call the `registerTagHandler` method, pass 72 | * the name of the tag and a Fully Qualified Class Name pointing to a class that implements the Tag interface. 73 | */ 74 | final class StandardTagFactory implements TagFactory 75 | { 76 | /** PCRE regular expression matching a tag name. */ 77 | public const REGEX_TAGNAME = '[\w\-\_\\\\:]+'; 78 | 79 | /** 80 | * @var array|Factory> An array with a tag as a key, and an 81 | * FQCN to a class that handles it as an array value. 82 | */ 83 | private array $tagHandlerMappings = [ 84 | 'author' => Author::class, 85 | 'covers' => Covers::class, 86 | 'deprecated' => Deprecated::class, 87 | // 'example' => '\phpDocumentor\Reflection\DocBlock\Tags\Example', 88 | 'link' => LinkTag::class, 89 | 'mixin' => Mixin::class, 90 | 'method' => Method::class, 91 | 'param' => Param::class, 92 | 'property-read' => PropertyRead::class, 93 | 'property' => Property::class, 94 | 'property-write' => PropertyWrite::class, 95 | 'return' => Return_::class, 96 | 'see' => SeeTag::class, 97 | 'since' => Since::class, 98 | 'source' => Source::class, 99 | 'template-covariant' => TemplateCovariant::class, 100 | 'throw' => Throws::class, 101 | 'throws' => Throws::class, 102 | 'uses' => Uses::class, 103 | 'var' => Var_::class, 104 | 'version' => Version::class, 105 | ]; 106 | 107 | /** 108 | * @var array> An array with an annotation as a key, and an 109 | * FQCN to a class that handles it as an array value. 110 | */ 111 | private array $annotationMappings = []; 112 | 113 | /** 114 | * @var ReflectionParameter[][] a lazy-loading cache containing parameters 115 | * for each tagHandler that has been used. 116 | */ 117 | private array $tagHandlerParameterCache = []; 118 | 119 | private FqsenResolver $fqsenResolver; 120 | 121 | /** 122 | * @var mixed[] an array representing a simple Service Locator where we can store parameters and 123 | * services that can be inserted into the Factory Methods of Tag Handlers. 124 | */ 125 | private array $serviceLocator = []; 126 | 127 | /** 128 | * Initialize this tag factory with the means to resolve an FQSEN and optionally a list of tag handlers. 129 | * 130 | * If no tag handlers are provided than the default list in the {@see self::$tagHandlerMappings} property 131 | * is used. 132 | * 133 | * @see self::registerTagHandler() to add a new tag handler to the existing default list. 134 | * 135 | * @param array> $tagHandlers 136 | */ 137 | public function __construct(FqsenResolver $fqsenResolver, ?array $tagHandlers = null) 138 | { 139 | $this->fqsenResolver = $fqsenResolver; 140 | if ($tagHandlers !== null) { 141 | $this->tagHandlerMappings = $tagHandlers; 142 | } 143 | 144 | $this->addService($fqsenResolver, FqsenResolver::class); 145 | } 146 | 147 | public function create(string $tagLine, ?TypeContext $context = null): Tag 148 | { 149 | if (!$context) { 150 | $context = new TypeContext(''); 151 | } 152 | 153 | [$tagName, $tagBody] = $this->extractTagParts($tagLine); 154 | 155 | return $this->createTag(trim($tagBody), $tagName, $context); 156 | } 157 | 158 | /** 159 | * @param mixed $value 160 | */ 161 | public function addParameter(string $name, $value): void 162 | { 163 | $this->serviceLocator[$name] = $value; 164 | } 165 | 166 | public function addService(object $service, ?string $alias = null): void 167 | { 168 | $this->serviceLocator[$alias ?? get_class($service)] = $service; 169 | } 170 | 171 | /** {@inheritDoc} */ 172 | public function registerTagHandler(string $tagName, $handler): void 173 | { 174 | Assert::stringNotEmpty($tagName); 175 | if (strpos($tagName, '\\') !== false && $tagName[0] !== '\\') { 176 | throw new InvalidArgumentException( 177 | 'A namespaced tag must have a leading backslash as it must be fully qualified' 178 | ); 179 | } 180 | 181 | if (is_object($handler)) { 182 | Assert::isInstanceOf($handler, Factory::class); 183 | $this->tagHandlerMappings[$tagName] = $handler; 184 | 185 | return; 186 | } 187 | 188 | Assert::classExists($handler); 189 | Assert::implementsInterface($handler, Tag::class); 190 | $this->tagHandlerMappings[$tagName] = $handler; 191 | } 192 | 193 | /** 194 | * Extracts all components for a tag. 195 | * 196 | * @return string[] 197 | */ 198 | private function extractTagParts(string $tagLine): array 199 | { 200 | $matches = []; 201 | if (!preg_match('/^@(' . self::REGEX_TAGNAME . ')((?:[\s\(\{])\s*([^\s].*)|$)/us', $tagLine, $matches)) { 202 | throw new InvalidArgumentException( 203 | 'The tag "' . $tagLine . '" does not seem to be wellformed, please check it for errors' 204 | ); 205 | } 206 | 207 | return array_slice($matches, 1); 208 | } 209 | 210 | /** 211 | * Creates a new tag object with the given name and body or returns null if the tag name was recognized but the 212 | * body was invalid. 213 | */ 214 | private function createTag(string $body, string $name, TypeContext $context): Tag 215 | { 216 | $handlerClassName = $this->findHandlerClassName($name, $context); 217 | $arguments = $this->getArgumentsForParametersFromWiring( 218 | $this->fetchParametersForHandlerFactoryMethod($handlerClassName), 219 | $this->getServiceLocatorWithDynamicParameters($context, $name, $body) 220 | ); 221 | 222 | if (array_key_exists('tagLine', $arguments)) { 223 | $arguments['tagLine'] = sprintf('@%s %s', $name, $body); 224 | } 225 | 226 | try { 227 | $callable = [$handlerClassName, 'create']; 228 | Assert::isCallable($callable); 229 | /** @phpstan-var callable(string): ?Tag $callable */ 230 | $tag = call_user_func_array($callable, $arguments); 231 | 232 | return $tag ?? InvalidTag::create($body, $name); 233 | } catch (InvalidArgumentException $e) { 234 | return InvalidTag::create($body, $name)->withError($e); 235 | } 236 | } 237 | 238 | /** 239 | * Determines the Fully Qualified Class Name of the Factory or Tag (containing a Factory Method `create`). 240 | * 241 | * @return class-string|Factory 242 | */ 243 | private function findHandlerClassName(string $tagName, TypeContext $context) 244 | { 245 | $handlerClassName = Generic::class; 246 | if (isset($this->tagHandlerMappings[$tagName])) { 247 | $handlerClassName = $this->tagHandlerMappings[$tagName]; 248 | } elseif ($this->isAnnotation($tagName)) { 249 | // TODO: Annotation support is planned for a later stage and as such is disabled for now 250 | $tagName = (string) $this->fqsenResolver->resolve($tagName, $context); 251 | if (isset($this->annotationMappings[$tagName])) { 252 | $handlerClassName = $this->annotationMappings[$tagName]; 253 | } 254 | } 255 | 256 | return $handlerClassName; 257 | } 258 | 259 | /** 260 | * Retrieves the arguments that need to be passed to the Factory Method with the given Parameters. 261 | * 262 | * @param ReflectionParameter[] $parameters 263 | * @param mixed[] $locator 264 | * 265 | * @return mixed[] A series of values that can be passed to the Factory Method of the tag whose parameters 266 | * is provided with this method. 267 | */ 268 | private function getArgumentsForParametersFromWiring(array $parameters, array $locator): array 269 | { 270 | $arguments = []; 271 | foreach ($parameters as $parameter) { 272 | $type = $parameter->getType(); 273 | $typeHint = null; 274 | if ($type instanceof ReflectionNamedType) { 275 | $typeHint = $type->getName(); 276 | if ($typeHint === 'self') { 277 | $declaringClass = $parameter->getDeclaringClass(); 278 | if ($declaringClass !== null) { 279 | $typeHint = $declaringClass->getName(); 280 | } 281 | } 282 | } 283 | 284 | $parameterName = $parameter->getName(); 285 | if (isset($locator[$typeHint])) { 286 | $arguments[$parameterName] = $locator[$typeHint]; 287 | continue; 288 | } 289 | 290 | if (isset($locator[$parameterName])) { 291 | $arguments[$parameterName] = $locator[$parameterName]; 292 | continue; 293 | } 294 | 295 | $arguments[$parameterName] = null; 296 | } 297 | 298 | return $arguments; 299 | } 300 | 301 | /** 302 | * Retrieves a series of ReflectionParameter objects for the static 'create' method of the given 303 | * tag handler class name. 304 | * 305 | * @param class-string|Factory $handler 306 | * 307 | * @return ReflectionParameter[] 308 | */ 309 | private function fetchParametersForHandlerFactoryMethod($handler): array 310 | { 311 | $handlerClassName = is_object($handler) ? get_class($handler) : $handler; 312 | 313 | if (!isset($this->tagHandlerParameterCache[$handlerClassName])) { 314 | $methodReflection = new ReflectionMethod($handlerClassName, 'create'); 315 | $this->tagHandlerParameterCache[$handlerClassName] = $methodReflection->getParameters(); 316 | } 317 | 318 | return $this->tagHandlerParameterCache[$handlerClassName]; 319 | } 320 | 321 | /** 322 | * Returns a copy of this class' Service Locator with added dynamic parameters, 323 | * such as the tag's name, body and Context. 324 | * 325 | * @param TypeContext $context The Context (namespace and aliases) that may be 326 | * passed and is used to resolve FQSENs. 327 | * @param string $tagName The name of the tag that may be 328 | * passed onto the factory method of the Tag class. 329 | * @param string $tagBody The body of the tag that may be 330 | * passed onto the factory method of the Tag class. 331 | * 332 | * @return mixed[] 333 | */ 334 | private function getServiceLocatorWithDynamicParameters( 335 | TypeContext $context, 336 | string $tagName, 337 | string $tagBody 338 | ): array { 339 | return array_merge( 340 | $this->serviceLocator, 341 | [ 342 | 'name' => $tagName, 343 | 'body' => $tagBody, 344 | TypeContext::class => $context, 345 | ] 346 | ); 347 | } 348 | 349 | /** 350 | * Returns whether the given tag belongs to an annotation. 351 | * 352 | * @todo this method should be populated once we implement Annotation notation support. 353 | */ 354 | private function isAnnotation(string $tagContent): bool 355 | { 356 | // 1. Contains a namespace separator 357 | // 2. Contains parenthesis 358 | // 3. Is present in a list of known annotations (make the algorithm smart by first checking is the last part 359 | // of the annotation class name matches the found tag name 360 | 361 | return false; 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /src/DocBlock/Tag.php: -------------------------------------------------------------------------------- 1 | |Factory $handler FQCN of handler. 64 | * 65 | * @throws InvalidArgumentException If the tag name is not a string. 66 | * @throws InvalidArgumentException If the tag name is namespaced (contains backslashes) but 67 | * does not start with a backslash. 68 | * @throws InvalidArgumentException If the handler is not a string. 69 | * @throws InvalidArgumentException If the handler is not an existing class. 70 | * @throws InvalidArgumentException If the handler does not implement the {@see Tag} interface. 71 | */ 72 | public function registerTagHandler(string $tagName, $handler): void; 73 | } 74 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Author.php: -------------------------------------------------------------------------------- 1 | authorName = $authorName; 48 | $this->authorEmail = $authorEmail; 49 | } 50 | 51 | /** 52 | * Gets the author's name. 53 | * 54 | * @return string The author's name. 55 | */ 56 | public function getAuthorName(): string 57 | { 58 | return $this->authorName; 59 | } 60 | 61 | /** 62 | * Returns the author's email. 63 | * 64 | * @return string The author's email. 65 | */ 66 | public function getEmail(): string 67 | { 68 | return $this->authorEmail; 69 | } 70 | 71 | /** 72 | * Returns this tag in string form. 73 | */ 74 | public function __toString(): string 75 | { 76 | if ($this->authorEmail) { 77 | $authorEmail = '<' . $this->authorEmail . '>'; 78 | } else { 79 | $authorEmail = ''; 80 | } 81 | 82 | $authorName = $this->authorName; 83 | 84 | return $authorName . ($authorEmail !== '' ? ($authorName !== '' ? ' ' : '') . $authorEmail : ''); 85 | } 86 | 87 | /** 88 | * Attempts to create a new Author object based on the tag body. 89 | */ 90 | public static function create(string $body): ?self 91 | { 92 | $splitTagContent = preg_match('/^([^\<]*)(?:\<([^\>]*)\>)?$/u', $body, $matches); 93 | if (!$splitTagContent) { 94 | return null; 95 | } 96 | 97 | $authorName = trim($matches[1]); 98 | $email = isset($matches[2]) ? trim($matches[2]) : ''; 99 | 100 | return new static($authorName, $email); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/BaseTag.php: -------------------------------------------------------------------------------- 1 | name; 38 | } 39 | 40 | public function getDescription(): ?Description 41 | { 42 | return $this->description; 43 | } 44 | 45 | public function render(?Formatter $formatter = null): string 46 | { 47 | if ($formatter === null) { 48 | $formatter = new Formatter\PassthroughFormatter(); 49 | } 50 | 51 | return $formatter->format($this); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Covers.php: -------------------------------------------------------------------------------- 1 | refers = $refers; 42 | $this->description = $description; 43 | } 44 | 45 | public static function create( 46 | string $body, 47 | ?DescriptionFactory $descriptionFactory = null, 48 | ?FqsenResolver $resolver = null, 49 | ?TypeContext $context = null 50 | ): self { 51 | Assert::stringNotEmpty($body); 52 | Assert::notNull($descriptionFactory); 53 | Assert::notNull($resolver); 54 | 55 | $parts = Utils::pregSplit('/\s+/Su', $body, 2); 56 | 57 | return new static( 58 | self::resolveFqsen($parts[0], $resolver, $context), 59 | $descriptionFactory->create($parts[1] ?? '', $context) 60 | ); 61 | } 62 | 63 | private static function resolveFqsen(string $parts, ?FqsenResolver $fqsenResolver, ?TypeContext $context): Fqsen 64 | { 65 | Assert::notNull($fqsenResolver); 66 | $fqsenParts = explode('::', $parts); 67 | $resolved = $fqsenResolver->resolve($fqsenParts[0], $context); 68 | 69 | if (!array_key_exists(1, $fqsenParts)) { 70 | return $resolved; 71 | } 72 | 73 | return new Fqsen($resolved . '::' . $fqsenParts[1]); 74 | } 75 | 76 | /** 77 | * Returns the structural element this tag refers to. 78 | */ 79 | public function getReference(): Fqsen 80 | { 81 | return $this->refers; 82 | } 83 | 84 | /** 85 | * Returns a string representation of this tag. 86 | */ 87 | public function __toString(): string 88 | { 89 | if ($this->description) { 90 | $description = $this->description->render(); 91 | } else { 92 | $description = ''; 93 | } 94 | 95 | $refers = (string) $this->refers; 96 | 97 | return $refers . ($description !== '' ? ($refers !== '' ? ' ' : '') . $description : ''); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Deprecated.php: -------------------------------------------------------------------------------- 1 | version = $version; 54 | $this->description = $description; 55 | } 56 | 57 | /** 58 | * @return static 59 | */ 60 | public static function create( 61 | ?string $body, 62 | ?DescriptionFactory $descriptionFactory = null, 63 | ?TypeContext $context = null 64 | ): self { 65 | if ($body === null || $body === '') { 66 | return new static(); 67 | } 68 | 69 | $matches = []; 70 | if (!preg_match('/^(' . self::REGEX_VECTOR . ')\s*(.+)?$/sux', $body, $matches)) { 71 | return new static( 72 | null, 73 | $descriptionFactory !== null ? $descriptionFactory->create($body, $context) : null 74 | ); 75 | } 76 | 77 | Assert::notNull($descriptionFactory); 78 | 79 | return new static( 80 | $matches[1], 81 | $descriptionFactory->create($matches[2] ?? '', $context) 82 | ); 83 | } 84 | 85 | /** 86 | * Gets the version section of the tag. 87 | */ 88 | public function getVersion(): ?string 89 | { 90 | return $this->version; 91 | } 92 | 93 | /** 94 | * Returns a string representation for this tag. 95 | */ 96 | public function __toString(): string 97 | { 98 | if ($this->description) { 99 | $description = $this->description->render(); 100 | } else { 101 | $description = ''; 102 | } 103 | 104 | $version = (string) $this->version; 105 | 106 | return $version . ($description !== '' ? ($version !== '' ? ' ' : '') . $description : ''); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Example.php: -------------------------------------------------------------------------------- 1 | filePath = $filePath; 58 | $this->startingLine = $startingLine; 59 | $this->lineCount = $lineCount; 60 | if ($content !== null) { 61 | $this->content = trim($content); 62 | } 63 | 64 | $this->isURI = $isURI; 65 | } 66 | 67 | public function getContent(): string 68 | { 69 | if ($this->content === null || $this->content === '') { 70 | $filePath = $this->filePath; 71 | if ($this->isURI) { 72 | $filePath = $this->isUriRelative($this->filePath) 73 | ? str_replace('%2F', '/', rawurlencode($this->filePath)) 74 | : $this->filePath; 75 | } 76 | 77 | return trim($filePath); 78 | } 79 | 80 | return $this->content; 81 | } 82 | 83 | public function getDescription(): ?string 84 | { 85 | return $this->content; 86 | } 87 | 88 | public static function create(string $body): ?Tag 89 | { 90 | // File component: File path in quotes or File URI / Source information 91 | if (!preg_match('/^\s*(?:(\"[^\"]+\")|(\S+))(?:\s+(.*))?$/sux', $body, $matches)) { 92 | return null; 93 | } 94 | 95 | $filePath = null; 96 | $fileUri = null; 97 | if (array_key_exists(1, $matches) && $matches[1] !== '') { 98 | $filePath = $matches[1]; 99 | } else { 100 | $fileUri = array_key_exists(2, $matches) ? $matches[2] : ''; 101 | } 102 | 103 | $startingLine = 1; 104 | $lineCount = 0; 105 | $description = null; 106 | 107 | if (array_key_exists(3, $matches)) { 108 | $description = $matches[3]; 109 | 110 | // Starting line / Number of lines / Description 111 | if (preg_match('/^([1-9]\d*)(?:\s+((?1))\s*)?(.*)$/sux', $matches[3], $contentMatches)) { 112 | $startingLine = (int) $contentMatches[1]; 113 | if (isset($contentMatches[2])) { 114 | $lineCount = (int) $contentMatches[2]; 115 | } 116 | 117 | if (array_key_exists(3, $contentMatches)) { 118 | $description = $contentMatches[3]; 119 | } 120 | } 121 | } 122 | 123 | return new static( 124 | $filePath ?? ($fileUri ?? ''), 125 | $fileUri !== null, 126 | $startingLine, 127 | $lineCount, 128 | $description 129 | ); 130 | } 131 | 132 | /** 133 | * Returns the file path. 134 | * 135 | * @return string Path to a file to use as an example. 136 | * May also be an absolute URI. 137 | */ 138 | public function getFilePath(): string 139 | { 140 | return trim($this->filePath, '"'); 141 | } 142 | 143 | /** 144 | * Returns a string representation for this tag. 145 | */ 146 | public function __toString(): string 147 | { 148 | $filePath = $this->filePath; 149 | $isDefaultLine = $this->startingLine === 1 && $this->lineCount === 0; 150 | $startingLine = !$isDefaultLine ? (string) $this->startingLine : ''; 151 | $lineCount = !$isDefaultLine ? (string) $this->lineCount : ''; 152 | $content = (string) $this->content; 153 | 154 | return $filePath 155 | . ($startingLine !== '' 156 | ? ($filePath !== '' ? ' ' : '') . $startingLine 157 | : '') 158 | . ($lineCount !== '' 159 | ? ($filePath !== '' || $startingLine !== '' ? ' ' : '') . $lineCount 160 | : '') 161 | . ($content !== '' 162 | ? ($filePath !== '' || $startingLine !== '' || $lineCount !== '' ? ' ' : '') . $content 163 | : ''); 164 | } 165 | 166 | /** 167 | * Returns true if the provided URI is relative or contains a complete scheme (and thus is absolute). 168 | */ 169 | private function isUriRelative(string $uri): bool 170 | { 171 | return strpos($uri, ':') === false; 172 | } 173 | 174 | public function getStartingLine(): int 175 | { 176 | return $this->startingLine; 177 | } 178 | 179 | public function getLineCount(): int 180 | { 181 | return $this->lineCount; 182 | } 183 | 184 | public function getName(): string 185 | { 186 | return 'example'; 187 | } 188 | 189 | public function render(?Formatter $formatter = null): string 190 | { 191 | if ($formatter === null) { 192 | $formatter = new Formatter\PassthroughFormatter(); 193 | } 194 | 195 | return $formatter->format($this); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Extends_.php: -------------------------------------------------------------------------------- 1 | name = 'extends'; 29 | $this->type = $type; 30 | $this->description = $description; 31 | } 32 | 33 | /** 34 | * @deprecated Create using static factory is deprecated, 35 | * this method should not be called directly by library consumers 36 | */ 37 | public static function create(string $body): ?Tag 38 | { 39 | Deprecation::trigger( 40 | 'phpdocumentor/reflection-docblock', 41 | 'https://github.com/phpDocumentor/ReflectionDocBlock/issues/361', 42 | 'Create using static factory is deprecated, this method should not be called directly 43 | by library consumers', 44 | ); 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/AbstractPHPStanFactory.php: -------------------------------------------------------------------------------- 1 | true, 'lines' => true]); 51 | $this->lexer = new Lexer($config); 52 | $constParser = new ConstExprParser($config); 53 | $this->parser = new PhpDocParser( 54 | $config, 55 | new TypeParser($config, $constParser), 56 | $constParser 57 | ); 58 | } else { 59 | $this->lexer = new Lexer(true); 60 | $constParser = new ConstExprParser(true, true, ['lines' => true, 'indexes' => true]); 61 | $this->parser = new PhpDocParser( 62 | new TypeParser($constParser, true, ['lines' => true, 'indexes' => true]), 63 | $constParser, 64 | true, 65 | true, 66 | ['lines' => true, 'indexes' => true], 67 | true 68 | ); 69 | } 70 | 71 | $this->factories = $factories; 72 | } 73 | 74 | public function create(string $tagLine, ?TypeContext $context = null): Tag 75 | { 76 | $tokens = $this->tokenizeLine($tagLine . "\n"); 77 | $ast = $this->parser->parseTag($tokens); 78 | if (property_exists($ast->value, 'description') === true) { 79 | $ast->value->setAttribute( 80 | 'description', 81 | rtrim($ast->value->description . $tokens->joinUntil(Lexer::TOKEN_END), "\n") 82 | ); 83 | } 84 | 85 | if ($context === null) { 86 | $context = new TypeContext(''); 87 | } 88 | 89 | try { 90 | foreach ($this->factories as $factory) { 91 | if ($factory->supports($ast, $context)) { 92 | return $factory->create($ast, $context); 93 | } 94 | } 95 | } catch (RuntimeException $e) { 96 | return InvalidTag::create((string) $ast->value, 'method')->withError($e); 97 | } 98 | 99 | return InvalidTag::create( 100 | (string) $ast->value, 101 | $ast->name 102 | ); 103 | } 104 | 105 | /** 106 | * Solve the issue with the lexer not tokenizing the line correctly 107 | * 108 | * This method is a workaround for the lexer that includes newline tokens with spaces. For 109 | * phpstan this isn't an issue, as it doesn't do a lot of things with the indentation of descriptions. 110 | * But for us is important to keep the indentation of the descriptions, so we need to fix the lexer output. 111 | */ 112 | private function tokenizeLine(string $tagLine): TokenIterator 113 | { 114 | $tokens = $this->lexer->tokenize($tagLine); 115 | $fixed = []; 116 | foreach ($tokens as $token) { 117 | if (($token[1] === Lexer::TOKEN_PHPDOC_EOL) && rtrim($token[0], " \t") !== $token[0]) { 118 | $fixed[] = [ 119 | rtrim($token[Lexer::VALUE_OFFSET], " \t"), 120 | Lexer::TOKEN_PHPDOC_EOL, 121 | $token[2] ?? 0, 122 | ]; 123 | $fixed[] = [ 124 | ltrim($token[Lexer::VALUE_OFFSET], "\n\r"), 125 | Lexer::TOKEN_HORIZONTAL_WS, 126 | ($token[2] ?? 0) + 1, 127 | ]; 128 | continue; 129 | } 130 | 131 | $fixed[] = $token; 132 | } 133 | 134 | return new TokenIterator($fixed); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/ExtendsFactory.php: -------------------------------------------------------------------------------- 1 | descriptionFactory = $descriptionFactory; 29 | $this->typeResolver = $typeResolver; 30 | } 31 | 32 | public function supports(PhpDocTagNode $node, Context $context): bool 33 | { 34 | return $node->value instanceof ExtendsTagValueNode && $node->name === '@extends'; 35 | } 36 | 37 | public function create(PhpDocTagNode $node, Context $context): Tag 38 | { 39 | $tagValue = $node->value; 40 | Assert::isInstanceOf($tagValue, ExtendsTagValueNode::class); 41 | 42 | $description = $tagValue->getAttribute('description'); 43 | if (is_string($description) === false) { 44 | $description = $tagValue->description; 45 | } 46 | 47 | return new Extends_( 48 | $this->typeResolver->createType($tagValue->type, $context), 49 | $this->descriptionFactory->create($description, $context) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/Factory.php: -------------------------------------------------------------------------------- 1 | descriptionFactory = $descriptionFactory; 29 | $this->typeResolver = $typeResolver; 30 | } 31 | 32 | public function supports(PhpDocTagNode $node, Context $context): bool 33 | { 34 | return $node->value instanceof ImplementsTagValueNode && $node->name === '@implements'; 35 | } 36 | 37 | public function create(PhpDocTagNode $node, Context $context): Tag 38 | { 39 | $tagValue = $node->value; 40 | Assert::isInstanceOf($tagValue, ImplementsTagValueNode::class); 41 | 42 | $description = $tagValue->getAttribute('description'); 43 | if (is_string($description) === false) { 44 | $description = $tagValue->description; 45 | } 46 | 47 | return new Implements_( 48 | $this->typeResolver->createType($tagValue->type, $context), 49 | $this->descriptionFactory->create($description, $context) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/MethodFactory.php: -------------------------------------------------------------------------------- 1 | descriptionFactory = $descriptionFactory; 35 | $this->typeResolver = $typeResolver; 36 | } 37 | 38 | public function create(PhpDocTagNode $node, Context $context): Tag 39 | { 40 | $tagValue = $node->value; 41 | Assert::isInstanceOf($tagValue, MethodTagValueNode::class); 42 | 43 | return new Method( 44 | $tagValue->methodName, 45 | [], 46 | $this->createReturnType($tagValue, $context), 47 | $tagValue->isStatic, 48 | $this->descriptionFactory->create($tagValue->description, $context), 49 | false, 50 | array_map( 51 | function (MethodTagValueParameterNode $param) use ($context) { 52 | return new MethodParameter( 53 | trim($param->parameterName, '$'), 54 | $param->type === null ? new Mixed_() : $this->typeResolver->createType( 55 | $param->type, 56 | $context 57 | ), 58 | $param->isReference, 59 | $param->isVariadic, 60 | $param->defaultValue === null ? 61 | MethodParameter::NO_DEFAULT_VALUE : 62 | (string) $param->defaultValue 63 | ); 64 | }, 65 | $tagValue->parameters 66 | ), 67 | ); 68 | } 69 | 70 | public function supports(PhpDocTagNode $node, Context $context): bool 71 | { 72 | return $node->value instanceof MethodTagValueNode; 73 | } 74 | 75 | private function createReturnType(MethodTagValueNode $tagValue, Context $context): Type 76 | { 77 | if ($tagValue->returnType === null) { 78 | return new Void_(); 79 | } 80 | 81 | return $this->typeResolver->createType($tagValue->returnType, $context); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/MethodParameterFactory.php: -------------------------------------------------------------------------------- 1 | {$method}($defaultValue); 38 | } 39 | 40 | return ''; 41 | } 42 | 43 | private function formatDouble(float $defaultValue): string 44 | { 45 | return var_export($defaultValue, true); 46 | } 47 | 48 | /** 49 | * @param mixed $defaultValue 50 | */ 51 | private function formatNull($defaultValue): string 52 | { 53 | return 'null'; 54 | } 55 | 56 | private function formatInteger(int $defaultValue): string 57 | { 58 | return var_export($defaultValue, true); 59 | } 60 | 61 | private function formatString(string $defaultValue): string 62 | { 63 | return var_export($defaultValue, true); 64 | } 65 | 66 | private function formatBoolean(bool $defaultValue): string 67 | { 68 | return var_export($defaultValue, true); 69 | } 70 | 71 | /** 72 | * @param array<(array|int|float|bool|string|object|null)> $defaultValue 73 | */ 74 | private function formatArray(array $defaultValue): string 75 | { 76 | $formatedValue = '['; 77 | 78 | foreach ($defaultValue as $key => $value) { 79 | $method = 'format' . ucfirst(gettype($value)); 80 | if (!method_exists($this, $method)) { 81 | continue; 82 | } 83 | 84 | $formatedValue .= $this->{$method}($value); 85 | 86 | if ($key === array_key_last($defaultValue)) { 87 | continue; 88 | } 89 | 90 | $formatedValue .= ','; 91 | } 92 | 93 | return $formatedValue . ']'; 94 | } 95 | 96 | private function formatObject(object $defaultValue): string 97 | { 98 | return 'new ' . get_class($defaultValue) . '()'; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/PHPStanFactory.php: -------------------------------------------------------------------------------- 1 | descriptionFactory = $descriptionFactory; 37 | $this->typeResolver = $typeResolver; 38 | } 39 | 40 | public function create(PhpDocTagNode $node, Context $context): Tag 41 | { 42 | $tagValue = $node->value; 43 | 44 | if ($tagValue instanceof InvalidTagValueNode) { 45 | Deprecation::trigger( 46 | 'phpdocumentor/reflection-docblock', 47 | 'https://github.com/phpDocumentor/ReflectionDocBlock/issues/362', 48 | sprintf( 49 | 'Param tag value "%s" is invalid, falling back to legacy parsing. Please update your docblocks.', 50 | $tagValue->value 51 | ) 52 | ); 53 | 54 | return Param::create($tagValue->value, $this->typeResolver, $this->descriptionFactory, $context); 55 | } 56 | 57 | Assert::isInstanceOfAny( 58 | $tagValue, 59 | [ 60 | ParamTagValueNode::class, 61 | TypelessParamTagValueNode::class, 62 | ] 63 | ); 64 | 65 | if (($tagValue->type ?? null) instanceof OffsetAccessTypeNode) { 66 | return InvalidTag::create( 67 | (string) $tagValue, 68 | 'param' 69 | ); 70 | } 71 | 72 | $description = $tagValue->getAttribute('description'); 73 | if (is_string($description) === false) { 74 | $description = $tagValue->description; 75 | } 76 | 77 | return new Param( 78 | trim($tagValue->parameterName, '$'), 79 | $this->typeResolver->createType($tagValue->type ?? new IdentifierTypeNode('mixed'), $context), 80 | $tagValue->isVariadic, 81 | $this->descriptionFactory->create($description, $context), 82 | $tagValue->isReference 83 | ); 84 | } 85 | 86 | public function supports(PhpDocTagNode $node, Context $context): bool 87 | { 88 | return $node->value instanceof ParamTagValueNode 89 | || $node->value instanceof TypelessParamTagValueNode 90 | || $node->name === '@param'; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/PropertyFactory.php: -------------------------------------------------------------------------------- 1 | descriptionFactory = $descriptionFactory; 30 | $this->typeResolver = $typeResolver; 31 | } 32 | 33 | public function create(PhpDocTagNode $node, Context $context): Tag 34 | { 35 | $tagValue = $node->value; 36 | Assert::isInstanceOf($tagValue, PropertyTagValueNode::class); 37 | 38 | $description = $tagValue->getAttribute('description'); 39 | if (is_string($description) === false) { 40 | $description = $tagValue->description; 41 | } 42 | 43 | return new Property( 44 | trim($tagValue->propertyName, '$'), 45 | $this->typeResolver->createType($tagValue->type, $context), 46 | $this->descriptionFactory->create($description, $context) 47 | ); 48 | } 49 | 50 | public function supports(PhpDocTagNode $node, Context $context): bool 51 | { 52 | return $node->value instanceof PropertyTagValueNode && $node->name === '@property'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/PropertyReadFactory.php: -------------------------------------------------------------------------------- 1 | typeResolver = $typeResolver; 30 | $this->descriptionFactory = $descriptionFactory; 31 | } 32 | 33 | public function create(PhpDocTagNode $node, Context $context): Tag 34 | { 35 | $tagValue = $node->value; 36 | Assert::isInstanceOf($tagValue, PropertyTagValueNode::class); 37 | 38 | $description = $tagValue->getAttribute('description'); 39 | if (is_string($description) === false) { 40 | $description = $tagValue->description; 41 | } 42 | 43 | return new PropertyRead( 44 | trim($tagValue->propertyName, '$'), 45 | $this->typeResolver->createType($tagValue->type, $context), 46 | $this->descriptionFactory->create($description, $context) 47 | ); 48 | } 49 | 50 | public function supports(PhpDocTagNode $node, Context $context): bool 51 | { 52 | return $node->value instanceof PropertyTagValueNode && $node->name === '@property-read'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/PropertyWriteFactory.php: -------------------------------------------------------------------------------- 1 | descriptionFactory = $descriptionFactory; 30 | $this->typeResolver = $typeResolver; 31 | } 32 | 33 | public function create(PhpDocTagNode $node, Context $context): Tag 34 | { 35 | $tagValue = $node->value; 36 | Assert::isInstanceOf($tagValue, PropertyTagValueNode::class); 37 | 38 | $description = $tagValue->getAttribute('description'); 39 | if (is_string($description) === false) { 40 | $description = $tagValue->description; 41 | } 42 | 43 | return new PropertyWrite( 44 | trim($tagValue->propertyName, '$'), 45 | $this->typeResolver->createType($tagValue->type, $context), 46 | $this->descriptionFactory->create($description, $context) 47 | ); 48 | } 49 | 50 | public function supports(PhpDocTagNode $node, Context $context): bool 51 | { 52 | return $node->value instanceof PropertyTagValueNode && $node->name === '@property-write'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/ReturnFactory.php: -------------------------------------------------------------------------------- 1 | descriptionFactory = $descriptionFactory; 29 | $this->typeResolver = $typeResolver; 30 | } 31 | 32 | public function create(PhpDocTagNode $node, Context $context): Tag 33 | { 34 | $tagValue = $node->value; 35 | Assert::isInstanceOf($tagValue, ReturnTagValueNode::class); 36 | 37 | $description = $tagValue->getAttribute('description'); 38 | if (is_string($description) === false) { 39 | $description = $tagValue->description; 40 | } 41 | 42 | return new Return_( 43 | $this->typeResolver->createType($tagValue->type, $context), 44 | $this->descriptionFactory->create($description, $context) 45 | ); 46 | } 47 | 48 | public function supports(PhpDocTagNode $node, Context $context): bool 49 | { 50 | return $node->value instanceof ReturnTagValueNode; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/StaticMethod.php: -------------------------------------------------------------------------------- 1 | descriptionFactory = $descriptionFactory; 29 | $this->typeResolver = $typeResolver; 30 | } 31 | 32 | public function supports(PhpDocTagNode $node, Context $context): bool 33 | { 34 | return $node->value instanceof ExtendsTagValueNode && $node->name === '@template-extends'; 35 | } 36 | 37 | public function create(PhpDocTagNode $node, Context $context): Tag 38 | { 39 | $tagValue = $node->value; 40 | Assert::isInstanceOf($tagValue, ExtendsTagValueNode::class); 41 | 42 | $description = $tagValue->getAttribute('description'); 43 | if (is_string($description) === false) { 44 | $description = $tagValue->description; 45 | } 46 | 47 | return new TemplateExtends( 48 | $this->typeResolver->createType($tagValue->type, $context), 49 | $this->descriptionFactory->create($description, $context) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/TemplateFactory.php: -------------------------------------------------------------------------------- 1 | descriptionFactory = $descriptionFactory; 29 | $this->typeResolver = $typeResolver; 30 | } 31 | 32 | public function create(PhpDocTagNode $node, Context $context): Tag 33 | { 34 | $tagValue = $node->value; 35 | 36 | Assert::isInstanceOf($tagValue, TemplateTagValueNode::class); 37 | $name = $tagValue->name; 38 | 39 | $description = $tagValue->getAttribute('description'); 40 | if (is_string($description) === false) { 41 | $description = $tagValue->description; 42 | } 43 | 44 | return new Template( 45 | $name, 46 | $this->typeResolver->createType($tagValue->bound, $context), 47 | $this->typeResolver->createType($tagValue->default, $context), 48 | $this->descriptionFactory->create($description, $context) 49 | ); 50 | } 51 | 52 | public function supports(PhpDocTagNode $node, Context $context): bool 53 | { 54 | return $node->value instanceof TemplateTagValueNode && $node->name === '@template'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/TemplateImplementsFactory.php: -------------------------------------------------------------------------------- 1 | descriptionFactory = $descriptionFactory; 29 | $this->typeResolver = $typeResolver; 30 | } 31 | 32 | public function supports(PhpDocTagNode $node, Context $context): bool 33 | { 34 | return $node->value instanceof ImplementsTagValueNode && $node->name === '@template-implements'; 35 | } 36 | 37 | public function create(PhpDocTagNode $node, Context $context): Tag 38 | { 39 | $tagValue = $node->value; 40 | Assert::isInstanceOf($tagValue, ImplementsTagValueNode::class); 41 | 42 | $description = $tagValue->getAttribute('description'); 43 | if (is_string($description) === false) { 44 | $description = $tagValue->description; 45 | } 46 | 47 | return new TemplateImplements( 48 | $this->typeResolver->createType($tagValue->type, $context), 49 | $this->descriptionFactory->create($description, $context) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Factory/VarFactory.php: -------------------------------------------------------------------------------- 1 | descriptionFactory = $descriptionFactory; 30 | $this->typeResolver = $typeResolver; 31 | } 32 | 33 | public function create(PhpDocTagNode $node, Context $context): Tag 34 | { 35 | $tagValue = $node->value; 36 | Assert::isInstanceOf($tagValue, VarTagValueNode::class); 37 | 38 | $description = $tagValue->getAttribute('description'); 39 | if (is_string($description) === false) { 40 | $description = $tagValue->description; 41 | } 42 | 43 | return new Var_( 44 | trim($tagValue->variableName, '$'), 45 | $this->typeResolver->createType($tagValue->type, $context), 46 | $this->descriptionFactory->create($description, $context) 47 | ); 48 | } 49 | 50 | public function supports(PhpDocTagNode $node, Context $context): bool 51 | { 52 | return $node->value instanceof VarTagValueNode; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Formatter.php: -------------------------------------------------------------------------------- 1 | maxLen = max($this->maxLen, strlen($tag->getName())); 35 | } 36 | } 37 | 38 | /** 39 | * Formats the given tag to return a simple plain text version. 40 | */ 41 | public function format(Tag $tag): string 42 | { 43 | return '@' . $tag->getName() . 44 | str_repeat( 45 | ' ', 46 | $this->maxLen - strlen($tag->getName()) + 1 47 | ) . 48 | $tag; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Formatter/PassthroughFormatter.php: -------------------------------------------------------------------------------- 1 | getName() . ' ' . $tag); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Generic.php: -------------------------------------------------------------------------------- 1 | validateTagName($name); 39 | 40 | $this->name = $name; 41 | $this->description = $description; 42 | } 43 | 44 | /** 45 | * Creates a new tag that represents any unknown tag type. 46 | * 47 | * @return static 48 | */ 49 | public static function create( 50 | string $body, 51 | string $name = '', 52 | ?DescriptionFactory $descriptionFactory = null, 53 | ?TypeContext $context = null 54 | ): self { 55 | Assert::stringNotEmpty($name); 56 | Assert::notNull($descriptionFactory); 57 | 58 | $description = $body !== '' ? $descriptionFactory->create($body, $context) : null; 59 | 60 | return new static($name, $description); 61 | } 62 | 63 | /** 64 | * Returns the tag as a serialized string 65 | */ 66 | public function __toString(): string 67 | { 68 | if ($this->description) { 69 | $description = $this->description->render(); 70 | } else { 71 | $description = ''; 72 | } 73 | 74 | return $description; 75 | } 76 | 77 | /** 78 | * Validates if the tag name matches the expected format, otherwise throws an exception. 79 | */ 80 | private function validateTagName(string $name): void 81 | { 82 | if (!preg_match('/^' . StandardTagFactory::REGEX_TAGNAME . '$/u', $name)) { 83 | throw new InvalidArgumentException( 84 | 'The tag name "' . $name . '" is not wellformed. Tags may only consist of letters, underscores, ' 85 | . 'hyphens and backslashes.' 86 | ); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Implements_.php: -------------------------------------------------------------------------------- 1 | name = 'implements'; 29 | $this->type = $type; 30 | $this->description = $description; 31 | } 32 | 33 | /** 34 | * @deprecated Create using static factory is deprecated, 35 | * this method should not be called directly by library consumers 36 | */ 37 | public static function create(string $body): ?Tag 38 | { 39 | Deprecation::trigger( 40 | 'phpdocumentor/reflection-docblock', 41 | 'https://github.com/phpDocumentor/ReflectionDocBlock/issues/361', 42 | 'Create using static factory is deprecated, this method should not be called directly 43 | by library consumers', 44 | ); 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/InvalidTag.php: -------------------------------------------------------------------------------- 1 | name = $name; 44 | $this->body = $body; 45 | } 46 | 47 | public function getException(): ?Throwable 48 | { 49 | return $this->throwable; 50 | } 51 | 52 | public function getName(): string 53 | { 54 | return $this->name; 55 | } 56 | 57 | public static function create(string $body, string $name = ''): self 58 | { 59 | return new self($name, $body); 60 | } 61 | 62 | public function withError(Throwable $exception): self 63 | { 64 | $this->flattenExceptionBacktrace($exception); 65 | $tag = new self($this->name, $this->body); 66 | $tag->throwable = $exception; 67 | 68 | return $tag; 69 | } 70 | 71 | /** 72 | * Removes all complex types from backtrace 73 | * 74 | * Not all objects are serializable. So we need to remove them from the 75 | * stored exception to be sure that we do not break existing library usage. 76 | */ 77 | private function flattenExceptionBacktrace(Throwable $exception): void 78 | { 79 | $traceProperty = (new ReflectionClass(Exception::class))->getProperty('trace'); 80 | $traceProperty->setAccessible(true); 81 | 82 | do { 83 | $trace = $exception->getTrace(); 84 | if (isset($trace[0]['args'])) { 85 | $trace = array_map( 86 | function (array $call): array { 87 | $call['args'] = array_map([$this, 'flattenArguments'], $call['args'] ?? []); 88 | 89 | return $call; 90 | }, 91 | $trace 92 | ); 93 | } 94 | 95 | $traceProperty->setValue($exception, $trace); 96 | $exception = $exception->getPrevious(); 97 | } while ($exception !== null); 98 | 99 | $traceProperty->setAccessible(false); 100 | } 101 | 102 | /** 103 | * @param mixed $value 104 | * 105 | * @return mixed 106 | * 107 | * @throws ReflectionException 108 | */ 109 | private function flattenArguments($value) 110 | { 111 | if ($value instanceof Closure) { 112 | $closureReflection = new ReflectionFunction($value); 113 | $value = sprintf( 114 | '(Closure at %s:%s)', 115 | $closureReflection->getFileName(), 116 | $closureReflection->getStartLine() 117 | ); 118 | } elseif (is_object($value)) { 119 | $value = sprintf('object(%s)', get_class($value)); 120 | } elseif (is_resource($value)) { 121 | $value = sprintf('resource(%s)', get_resource_type($value)); 122 | } elseif (is_array($value)) { 123 | $value = array_map([$this, 'flattenArguments'], $value); 124 | } 125 | 126 | return $value; 127 | } 128 | 129 | public function render(?Formatter $formatter = null): string 130 | { 131 | if ($formatter === null) { 132 | $formatter = new Formatter\PassthroughFormatter(); 133 | } 134 | 135 | return $formatter->format($this); 136 | } 137 | 138 | public function __toString(): string 139 | { 140 | return $this->body; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Link.php: -------------------------------------------------------------------------------- 1 | link = $link; 37 | $this->description = $description; 38 | } 39 | 40 | public static function create( 41 | string $body, 42 | ?DescriptionFactory $descriptionFactory = null, 43 | ?TypeContext $context = null 44 | ): self { 45 | Assert::notNull($descriptionFactory); 46 | 47 | $parts = Utils::pregSplit('/\s+/Su', $body, 2); 48 | $description = isset($parts[1]) ? $descriptionFactory->create($parts[1], $context) : null; 49 | 50 | return new static($parts[0], $description); 51 | } 52 | 53 | /** 54 | * Gets the link 55 | */ 56 | public function getLink(): string 57 | { 58 | return $this->link; 59 | } 60 | 61 | /** 62 | * Returns a string representation for this tag. 63 | */ 64 | public function __toString(): string 65 | { 66 | if ($this->description) { 67 | $description = $this->description->render(); 68 | } else { 69 | $description = ''; 70 | } 71 | 72 | $link = $this->link; 73 | 74 | return $link . ($description !== '' ? ($link !== '' ? ' ' : '') . $description : ''); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Method.php: -------------------------------------------------------------------------------- 1 | > $arguments 61 | * @param MethodParameter[] $parameters 62 | * @phpstan-param array $arguments 63 | */ 64 | public function __construct( 65 | string $methodName, 66 | array $arguments = [], 67 | ?Type $returnType = null, 68 | bool $static = false, 69 | ?Description $description = null, 70 | bool $returnsReference = false, 71 | ?array $parameters = null 72 | ) { 73 | Assert::stringNotEmpty($methodName); 74 | 75 | if ($returnType === null) { 76 | $returnType = new Void_(); 77 | } 78 | 79 | $arguments = $this->filterArguments($arguments); 80 | 81 | $this->methodName = $methodName; 82 | $this->returnType = $returnType; 83 | $this->isStatic = $static; 84 | $this->description = $description; 85 | $this->returnsReference = $returnsReference; 86 | $this->parameters = $parameters ?? $this->fromLegacyArguments($arguments); 87 | } 88 | 89 | /** 90 | * @deprecated Create using static factory is deprecated, 91 | * this method should not be called directly by library consumers 92 | */ 93 | public static function create( 94 | string $body, 95 | ?TypeResolver $typeResolver = null, 96 | ?DescriptionFactory $descriptionFactory = null, 97 | ?TypeContext $context = null 98 | ): ?self { 99 | trigger_error( 100 | 'Create using static factory is deprecated, this method should not be called directly 101 | by library consumers', 102 | E_USER_DEPRECATED 103 | ); 104 | Assert::stringNotEmpty($body); 105 | Assert::notNull($typeResolver); 106 | Assert::notNull($descriptionFactory); 107 | 108 | // 1. none or more whitespace 109 | // 2. optionally the keyword "static" followed by whitespace 110 | // 3. optionally a word with underscores followed by whitespace : as 111 | // type for the return value 112 | // 4. optionally an ampersand followed or not by whitespace : as 113 | // a reference 114 | // 5. then optionally a word with underscores followed by () and 115 | // whitespace : as method name as used by phpDocumentor 116 | // 6. then a word with underscores, followed by ( and any character 117 | // until a ) and whitespace : as method name with signature 118 | // 7. any remaining text : as description 119 | if ( 120 | !preg_match( 121 | '/^ 122 | # Static keyword 123 | # Declares a static method ONLY if type is also present 124 | (?: 125 | (static) 126 | \s+ 127 | )? 128 | # Return type 129 | (?: 130 | ( 131 | (?:[\w\|_\\\\]*\$this[\w\|_\\\\]*) 132 | | 133 | (?: 134 | (?:[\w\|_\\\\]+) 135 | # array notation 136 | (?:\[\])* 137 | )*+ 138 | ) 139 | \s+ 140 | )? 141 | # Returns reference 142 | (?: 143 | (&) 144 | \s* 145 | )? 146 | # Method name 147 | ([\w_]+) 148 | # Arguments 149 | (?: 150 | \(([^\)]*)\) 151 | )? 152 | \s* 153 | # Description 154 | (.*) 155 | $/sux', 156 | $body, 157 | $matches 158 | ) 159 | ) { 160 | return null; 161 | } 162 | 163 | [, $static, $returnType, $returnsReference, $methodName, $argumentLines, $description] = $matches; 164 | 165 | $static = $static === 'static'; 166 | 167 | if ($returnType === '') { 168 | $returnType = 'void'; 169 | } 170 | 171 | $returnsReference = $returnsReference === '&'; 172 | 173 | $returnType = $typeResolver->resolve($returnType, $context); 174 | $description = $descriptionFactory->create($description, $context); 175 | 176 | /** @phpstan-var array $arguments */ 177 | $arguments = []; 178 | if ($argumentLines !== '') { 179 | $argumentsExploded = explode(',', $argumentLines); 180 | foreach ($argumentsExploded as $argument) { 181 | $argument = explode(' ', self::stripRestArg(trim($argument)), 2); 182 | if (strpos($argument[0], '$') === 0) { 183 | $argumentName = substr($argument[0], 1); 184 | $argumentType = new Mixed_(); 185 | } else { 186 | $argumentType = $typeResolver->resolve($argument[0], $context); 187 | $argumentName = ''; 188 | if (isset($argument[1])) { 189 | $argument[1] = self::stripRestArg($argument[1]); 190 | $argumentName = substr($argument[1], 1); 191 | } 192 | } 193 | 194 | $arguments[] = ['name' => $argumentName, 'type' => $argumentType]; 195 | } 196 | } 197 | 198 | return new static( 199 | $methodName, 200 | $arguments, 201 | $returnType, 202 | $static, 203 | $description, 204 | $returnsReference 205 | ); 206 | } 207 | 208 | /** 209 | * Retrieves the method name. 210 | */ 211 | public function getMethodName(): string 212 | { 213 | return $this->methodName; 214 | } 215 | 216 | /** 217 | * @deprecated Method deprecated, use {@see self::getParameters()} 218 | * 219 | * @return array> 220 | * @phpstan-return array 221 | */ 222 | public function getArguments(): array 223 | { 224 | trigger_error('Method deprecated, use ::getParameters()', E_USER_DEPRECATED); 225 | 226 | return array_map( 227 | static function (MethodParameter $methodParameter) { 228 | return ['name' => $methodParameter->getName(), 'type' => $methodParameter->getType()]; 229 | }, 230 | $this->parameters 231 | ); 232 | } 233 | 234 | /** @return MethodParameter[] */ 235 | public function getParameters(): array 236 | { 237 | return $this->parameters; 238 | } 239 | 240 | /** 241 | * Checks whether the method tag describes a static method or not. 242 | * 243 | * @return bool TRUE if the method declaration is for a static method, FALSE otherwise. 244 | */ 245 | public function isStatic(): bool 246 | { 247 | return $this->isStatic; 248 | } 249 | 250 | public function getReturnType(): Type 251 | { 252 | return $this->returnType; 253 | } 254 | 255 | public function returnsReference(): bool 256 | { 257 | return $this->returnsReference; 258 | } 259 | 260 | public function __toString(): string 261 | { 262 | $arguments = []; 263 | foreach ($this->parameters as $parameter) { 264 | $arguments[] = (string) $parameter; 265 | } 266 | 267 | $argumentStr = '(' . implode(', ', $arguments) . ')'; 268 | 269 | if ($this->description) { 270 | $description = $this->description->render(); 271 | } else { 272 | $description = ''; 273 | } 274 | 275 | $static = $this->isStatic ? 'static' : ''; 276 | 277 | $returnType = (string) $this->returnType; 278 | 279 | $methodName = $this->methodName; 280 | 281 | $reference = $this->returnsReference ? '&' : ''; 282 | 283 | return $static 284 | . ($returnType !== '' ? ($static !== '' ? ' ' : '') . $returnType : '') 285 | . ($methodName !== '' ? ($static !== '' || $returnType !== '' ? ' ' : '') . $reference . $methodName : '') 286 | . $argumentStr 287 | . ($description !== '' ? ' ' . $description : ''); 288 | } 289 | 290 | /** 291 | * @param mixed[][]|string[] $arguments 292 | * @phpstan-param array $arguments 293 | * 294 | * @return mixed[][] 295 | * @phpstan-return array 296 | */ 297 | private function filterArguments(array $arguments = []): array 298 | { 299 | $result = []; 300 | foreach ($arguments as $argument) { 301 | if (is_string($argument)) { 302 | $argument = ['name' => $argument]; 303 | } 304 | 305 | if (!isset($argument['type'])) { 306 | $argument['type'] = new Mixed_(); 307 | } 308 | 309 | $keys = array_keys($argument); 310 | sort($keys); 311 | if ($keys !== ['name', 'type']) { 312 | throw new InvalidArgumentException( 313 | 'Arguments can only have the "name" and "type" fields, found: ' . var_export($keys, true) 314 | ); 315 | } 316 | 317 | $result[] = $argument; 318 | } 319 | 320 | return $result; 321 | } 322 | 323 | private static function stripRestArg(string $argument): string 324 | { 325 | if (strpos($argument, '...') === 0) { 326 | $argument = trim(substr($argument, 3)); 327 | } 328 | 329 | return $argument; 330 | } 331 | 332 | /** 333 | * @param array{name: string, type: Type} $arguments 334 | * @phpstan-param array $arguments 335 | * 336 | * @return MethodParameter[] 337 | */ 338 | private function fromLegacyArguments(array $arguments): array 339 | { 340 | trigger_error( 341 | 'Create method parameters via legacy format is deprecated add parameters via the constructor', 342 | E_USER_DEPRECATED 343 | ); 344 | 345 | return array_map( 346 | static function ($arg) { 347 | return new MethodParameter( 348 | $arg['name'], 349 | $arg['type'] 350 | ); 351 | }, 352 | $arguments 353 | ); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/MethodParameter.php: -------------------------------------------------------------------------------- 1 | type = $type; 44 | $this->isReference = $isReference; 45 | $this->isVariadic = $isVariadic; 46 | $this->name = $name; 47 | $this->defaultValue = $defaultValue; 48 | } 49 | 50 | public function getName(): string 51 | { 52 | return $this->name; 53 | } 54 | 55 | public function getType(): Type 56 | { 57 | return $this->type; 58 | } 59 | 60 | public function isReference(): bool 61 | { 62 | return $this->isReference; 63 | } 64 | 65 | public function isVariadic(): bool 66 | { 67 | return $this->isVariadic; 68 | } 69 | 70 | public function getDefaultValue(): ?string 71 | { 72 | if ($this->defaultValue === self::NO_DEFAULT_VALUE) { 73 | return null; 74 | } 75 | 76 | return (new MethodParameterFactory())->format($this->defaultValue); 77 | } 78 | 79 | public function __toString(): string 80 | { 81 | return $this->getType() . ' ' . 82 | ($this->isReference() ? '&' : '') . 83 | ($this->isVariadic() ? '...' : '') . 84 | '$' . $this->getName() . 85 | ( 86 | $this->defaultValue !== self::NO_DEFAULT_VALUE ? 87 | (new MethodParameterFactory())->format($this->defaultValue) : 88 | '' 89 | ); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Mixin.php: -------------------------------------------------------------------------------- 1 | name = 'mixin'; 31 | $this->type = $type; 32 | $this->description = $description; 33 | } 34 | 35 | public static function create( 36 | string $body, 37 | ?TypeResolver $typeResolver = null, 38 | ?DescriptionFactory $descriptionFactory = null, 39 | ?TypeContext $context = null 40 | ): self { 41 | Assert::notNull($typeResolver); 42 | Assert::notNull($descriptionFactory); 43 | 44 | [$type, $description] = self::extractTypeFromBody($body); 45 | 46 | $type = $typeResolver->resolve($type, $context); 47 | $description = $descriptionFactory->create($description, $context); 48 | 49 | return new static($type, $description); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Param.php: -------------------------------------------------------------------------------- 1 | name = 'param'; 54 | $this->variableName = $variableName; 55 | $this->type = $type; 56 | $this->isVariadic = $isVariadic; 57 | $this->description = $description; 58 | $this->isReference = $isReference; 59 | } 60 | 61 | /** 62 | * @deprecated Create using static factory is deprecated, 63 | * this method should not be called directly by library consumers 64 | */ 65 | public static function create( 66 | string $body, 67 | ?TypeResolver $typeResolver = null, 68 | ?DescriptionFactory $descriptionFactory = null, 69 | ?TypeContext $context = null 70 | ): self { 71 | Deprecation::triggerIfCalledFromOutside( 72 | 'phpdocumentor/reflection-docblock', 73 | 'https://github.com/phpDocumentor/ReflectionDocBlock/issues/361', 74 | 'Create using static factory is deprecated, this method should not be called directly 75 | by library consumers', 76 | ); 77 | 78 | Assert::stringNotEmpty($body); 79 | Assert::notNull($typeResolver); 80 | Assert::notNull($descriptionFactory); 81 | 82 | [$firstPart, $body] = self::extractTypeFromBody($body); 83 | 84 | $type = null; 85 | $parts = Utils::pregSplit('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE); 86 | $variableName = ''; 87 | $isVariadic = false; 88 | $isReference = false; 89 | 90 | // if the first item that is encountered is not a variable; it is a type 91 | if ($firstPart && !self::strStartsWithVariable($firstPart)) { 92 | $type = $typeResolver->resolve($firstPart, $context); 93 | } else { 94 | // first part is not a type; we should prepend it to the parts array for further processing 95 | array_unshift($parts, $firstPart); 96 | } 97 | 98 | // if the next item starts with a $ or ...$ or &$ or &...$ it must be the variable name 99 | if (isset($parts[0]) && self::strStartsWithVariable($parts[0])) { 100 | $variableName = array_shift($parts); 101 | if ($type) { 102 | array_shift($parts); 103 | } 104 | 105 | Assert::notNull($variableName); 106 | 107 | if (strpos($variableName, '$') === 0) { 108 | $variableName = substr($variableName, 1); 109 | } elseif (strpos($variableName, '&$') === 0) { 110 | $isReference = true; 111 | $variableName = substr($variableName, 2); 112 | } elseif (strpos($variableName, '...$') === 0) { 113 | $isVariadic = true; 114 | $variableName = substr($variableName, 4); 115 | } elseif (strpos($variableName, '&...$') === 0) { 116 | $isVariadic = true; 117 | $isReference = true; 118 | $variableName = substr($variableName, 5); 119 | } 120 | } 121 | 122 | $description = $descriptionFactory->create(implode('', $parts), $context); 123 | 124 | return new static($variableName, $type, $isVariadic, $description, $isReference); 125 | } 126 | 127 | /** 128 | * Returns the variable's name. 129 | */ 130 | public function getVariableName(): ?string 131 | { 132 | return $this->variableName; 133 | } 134 | 135 | /** 136 | * Returns whether this tag is variadic. 137 | */ 138 | public function isVariadic(): bool 139 | { 140 | return $this->isVariadic; 141 | } 142 | 143 | /** 144 | * Returns whether this tag is passed by reference. 145 | */ 146 | public function isReference(): bool 147 | { 148 | return $this->isReference; 149 | } 150 | 151 | /** 152 | * Returns a string representation for this tag. 153 | */ 154 | public function __toString(): string 155 | { 156 | if ($this->description) { 157 | $description = $this->description->render(); 158 | } else { 159 | $description = ''; 160 | } 161 | 162 | $variableName = ''; 163 | if ($this->variableName !== null && $this->variableName !== '') { 164 | $variableName .= ($this->isReference ? '&' : '') . ($this->isVariadic ? '...' : ''); 165 | $variableName .= '$' . $this->variableName; 166 | } 167 | 168 | $type = (string) $this->type; 169 | 170 | return $type 171 | . ($variableName !== '' ? ($type !== '' ? ' ' : '') . $variableName : '') 172 | . ($description !== '' ? ($type !== '' || $variableName !== '' ? ' ' : '') . $description : ''); 173 | } 174 | 175 | private static function strStartsWithVariable(string $str): bool 176 | { 177 | return strpos($str, '$') === 0 178 | || 179 | strpos($str, '...$') === 0 180 | || 181 | strpos($str, '&$') === 0 182 | || 183 | strpos($str, '&...$') === 0; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Property.php: -------------------------------------------------------------------------------- 1 | name = 'property'; 45 | $this->variableName = $variableName; 46 | $this->type = $type; 47 | $this->description = $description; 48 | } 49 | 50 | /** 51 | * @deprecated Create using static factory is deprecated, 52 | * this method should not be called directly by library consumers 53 | */ 54 | public static function create( 55 | string $body, 56 | ?TypeResolver $typeResolver = null, 57 | ?DescriptionFactory $descriptionFactory = null, 58 | ?TypeContext $context = null 59 | ): self { 60 | Deprecation::triggerIfCalledFromOutside( 61 | 'phpdocumentor/reflection-docblock', 62 | 'https://github.com/phpDocumentor/ReflectionDocBlock/issues/361', 63 | 'Create using static factory is deprecated, this method should not be called directly 64 | by library consumers', 65 | ); 66 | 67 | Assert::stringNotEmpty($body); 68 | Assert::notNull($typeResolver); 69 | Assert::notNull($descriptionFactory); 70 | 71 | [$firstPart, $body] = self::extractTypeFromBody($body); 72 | $type = null; 73 | $parts = Utils::pregSplit('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE); 74 | $variableName = ''; 75 | 76 | // if the first item that is encountered is not a variable; it is a type 77 | if ($firstPart && $firstPart[0] !== '$') { 78 | $type = $typeResolver->resolve($firstPart, $context); 79 | } else { 80 | // first part is not a type; we should prepend it to the parts array for further processing 81 | array_unshift($parts, $firstPart); 82 | } 83 | 84 | // if the next item starts with a $ it must be the variable name 85 | if (isset($parts[0]) && strpos($parts[0], '$') === 0) { 86 | $variableName = array_shift($parts); 87 | if ($type) { 88 | array_shift($parts); 89 | } 90 | 91 | Assert::notNull($variableName); 92 | 93 | $variableName = substr($variableName, 1); 94 | } 95 | 96 | $description = $descriptionFactory->create(implode('', $parts), $context); 97 | 98 | return new static($variableName, $type, $description); 99 | } 100 | 101 | /** 102 | * Returns the variable's name. 103 | */ 104 | public function getVariableName(): ?string 105 | { 106 | return $this->variableName; 107 | } 108 | 109 | /** 110 | * Returns a string representation for this tag. 111 | */ 112 | public function __toString(): string 113 | { 114 | if ($this->description !== null) { 115 | $description = $this->description->render(); 116 | } else { 117 | $description = ''; 118 | } 119 | 120 | if ($this->variableName !== null && $this->variableName !== '') { 121 | $variableName = '$' . $this->variableName; 122 | } else { 123 | $variableName = ''; 124 | } 125 | 126 | $type = (string) $this->type; 127 | 128 | return $type 129 | . ($variableName !== '' ? ($type !== '' ? ' ' : '') . $variableName : '') 130 | . ($description !== '' ? ($type !== '' || $variableName !== '' ? ' ' : '') . $description : ''); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/PropertyRead.php: -------------------------------------------------------------------------------- 1 | name = 'property-read'; 45 | $this->variableName = $variableName; 46 | $this->type = $type; 47 | $this->description = $description; 48 | } 49 | 50 | /** 51 | * @deprecated Create using static factory is deprecated, 52 | * this method should not be called directly by library consumers 53 | */ 54 | public static function create( 55 | string $body, 56 | ?TypeResolver $typeResolver = null, 57 | ?DescriptionFactory $descriptionFactory = null, 58 | ?TypeContext $context = null 59 | ): self { 60 | Deprecation::triggerIfCalledFromOutside( 61 | 'phpdocumentor/reflection-docblock', 62 | 'https://github.com/phpDocumentor/ReflectionDocBlock/issues/361', 63 | 'Create using static factory is deprecated, this method should not be called directly 64 | by library consumers', 65 | ); 66 | 67 | Assert::stringNotEmpty($body); 68 | Assert::notNull($typeResolver); 69 | Assert::notNull($descriptionFactory); 70 | 71 | [$firstPart, $body] = self::extractTypeFromBody($body); 72 | $type = null; 73 | $parts = Utils::pregSplit('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE); 74 | $variableName = ''; 75 | 76 | // if the first item that is encountered is not a variable; it is a type 77 | if ($firstPart && $firstPart[0] !== '$') { 78 | $type = $typeResolver->resolve($firstPart, $context); 79 | } else { 80 | // first part is not a type; we should prepend it to the parts array for further processing 81 | array_unshift($parts, $firstPart); 82 | } 83 | 84 | // if the next item starts with a $ it must be the variable name 85 | if (isset($parts[0]) && strpos($parts[0], '$') === 0) { 86 | $variableName = array_shift($parts); 87 | if ($type) { 88 | array_shift($parts); 89 | } 90 | 91 | Assert::notNull($variableName); 92 | 93 | $variableName = substr($variableName, 1); 94 | } 95 | 96 | $description = $descriptionFactory->create(implode('', $parts), $context); 97 | 98 | return new static($variableName, $type, $description); 99 | } 100 | 101 | /** 102 | * Returns the variable's name. 103 | */ 104 | public function getVariableName(): ?string 105 | { 106 | return $this->variableName; 107 | } 108 | 109 | /** 110 | * Returns a string representation for this tag. 111 | */ 112 | public function __toString(): string 113 | { 114 | if ($this->description !== null) { 115 | $description = $this->description->render(); 116 | } else { 117 | $description = ''; 118 | } 119 | 120 | if ($this->variableName !== null && $this->variableName !== '') { 121 | $variableName = '$' . $this->variableName; 122 | } else { 123 | $variableName = ''; 124 | } 125 | 126 | $type = (string) $this->type; 127 | 128 | return $type 129 | . ($variableName !== '' ? ($type !== '' ? ' ' : '') . $variableName : '') 130 | . ($description !== '' ? ($type !== '' || $variableName !== '' ? ' ' : '') . $description : ''); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/PropertyWrite.php: -------------------------------------------------------------------------------- 1 | name = 'property-write'; 45 | $this->variableName = $variableName; 46 | $this->type = $type; 47 | $this->description = $description; 48 | } 49 | 50 | /** 51 | * @deprecated Create using static factory is deprecated, 52 | * this method should not be called directly by library consumers 53 | */ 54 | public static function create( 55 | string $body, 56 | ?TypeResolver $typeResolver = null, 57 | ?DescriptionFactory $descriptionFactory = null, 58 | ?TypeContext $context = null 59 | ): self { 60 | Deprecation::triggerIfCalledFromOutside( 61 | 'phpdocumentor/reflection-docblock', 62 | 'https://github.com/phpDocumentor/ReflectionDocBlock/issues/361', 63 | 'Create using static factory is deprecated, this method should not be called directly 64 | by library consumers', 65 | ); 66 | 67 | Assert::stringNotEmpty($body); 68 | Assert::notNull($typeResolver); 69 | Assert::notNull($descriptionFactory); 70 | 71 | [$firstPart, $body] = self::extractTypeFromBody($body); 72 | $type = null; 73 | $parts = Utils::pregSplit('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE); 74 | $variableName = ''; 75 | 76 | // if the first item that is encountered is not a variable; it is a type 77 | if ($firstPart && $firstPart[0] !== '$') { 78 | $type = $typeResolver->resolve($firstPart, $context); 79 | } else { 80 | // first part is not a type; we should prepend it to the parts array for further processing 81 | array_unshift($parts, $firstPart); 82 | } 83 | 84 | // if the next item starts with a $ it must be the variable name 85 | if (isset($parts[0]) && strpos($parts[0], '$') === 0) { 86 | $variableName = array_shift($parts); 87 | if ($type) { 88 | array_shift($parts); 89 | } 90 | 91 | Assert::notNull($variableName); 92 | 93 | $variableName = substr($variableName, 1); 94 | } 95 | 96 | $description = $descriptionFactory->create(implode('', $parts), $context); 97 | 98 | return new static($variableName, $type, $description); 99 | } 100 | 101 | /** 102 | * Returns the variable's name. 103 | */ 104 | public function getVariableName(): ?string 105 | { 106 | return $this->variableName; 107 | } 108 | 109 | /** 110 | * Returns a string representation for this tag. 111 | */ 112 | public function __toString(): string 113 | { 114 | if ($this->description) { 115 | $description = $this->description->render(); 116 | } else { 117 | $description = ''; 118 | } 119 | 120 | if ($this->variableName) { 121 | $variableName = '$' . $this->variableName; 122 | } else { 123 | $variableName = ''; 124 | } 125 | 126 | $type = (string) $this->type; 127 | 128 | return $type 129 | . ($variableName !== '' ? ($type !== '' ? ' ' : '') . $variableName : '') 130 | . ($description !== '' ? ($type !== '' || $variableName !== '' ? ' ' : '') . $description : ''); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Reference/Fqsen.php: -------------------------------------------------------------------------------- 1 | fqsen = $fqsen; 28 | } 29 | 30 | /** 31 | * @return string string representation of the referenced fqsen 32 | */ 33 | public function __toString(): string 34 | { 35 | return (string) $this->fqsen; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Reference/Reference.php: -------------------------------------------------------------------------------- 1 | uri = $uri; 29 | } 30 | 31 | public function __toString(): string 32 | { 33 | return $this->uri; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Return_.php: -------------------------------------------------------------------------------- 1 | name = 'return'; 32 | $this->type = $type; 33 | $this->description = $description; 34 | } 35 | 36 | /** 37 | * @deprecated Create using static factory is deprecated, 38 | * this method should not be called directly by library consumers 39 | */ 40 | public static function create( 41 | string $body, 42 | ?TypeResolver $typeResolver = null, 43 | ?DescriptionFactory $descriptionFactory = null, 44 | ?TypeContext $context = null 45 | ): self { 46 | Deprecation::triggerIfCalledFromOutside( 47 | 'phpdocumentor/reflection-docblock', 48 | 'https://github.com/phpDocumentor/ReflectionDocBlock/issues/361', 49 | 'Create using static factory is deprecated, this method should not be called directly 50 | by library consumers', 51 | ); 52 | 53 | Assert::notNull($typeResolver); 54 | Assert::notNull($descriptionFactory); 55 | 56 | [$type, $description] = self::extractTypeFromBody($body); 57 | 58 | $type = $typeResolver->resolve($type, $context); 59 | $description = $descriptionFactory->create($description, $context); 60 | 61 | return new static($type, $description); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/See.php: -------------------------------------------------------------------------------- 1 | refers = $refers; 46 | $this->description = $description; 47 | } 48 | 49 | public static function create( 50 | string $body, 51 | ?FqsenResolver $typeResolver = null, 52 | ?DescriptionFactory $descriptionFactory = null, 53 | ?TypeContext $context = null 54 | ): self { 55 | Assert::notNull($descriptionFactory); 56 | 57 | $parts = Utils::pregSplit('/\s+/Su', $body, 2); 58 | $description = isset($parts[1]) ? $descriptionFactory->create($parts[1], $context) : null; 59 | 60 | // https://tools.ietf.org/html/rfc2396#section-3 61 | if (preg_match('#\w://\w#', $parts[0])) { 62 | return new static(new Url($parts[0]), $description); 63 | } 64 | 65 | return new static(new FqsenRef(self::resolveFqsen($parts[0], $typeResolver, $context)), $description); 66 | } 67 | 68 | private static function resolveFqsen(string $parts, ?FqsenResolver $fqsenResolver, ?TypeContext $context): Fqsen 69 | { 70 | Assert::notNull($fqsenResolver); 71 | $fqsenParts = explode('::', $parts); 72 | $resolved = $fqsenResolver->resolve($fqsenParts[0], $context); 73 | 74 | if (!array_key_exists(1, $fqsenParts)) { 75 | return $resolved; 76 | } 77 | 78 | return new Fqsen($resolved . '::' . $fqsenParts[1]); 79 | } 80 | 81 | /** 82 | * Returns the ref of this tag. 83 | */ 84 | public function getReference(): Reference 85 | { 86 | return $this->refers; 87 | } 88 | 89 | /** 90 | * Returns a string representation of this tag. 91 | */ 92 | public function __toString(): string 93 | { 94 | if ($this->description) { 95 | $description = $this->description->render(); 96 | } else { 97 | $description = ''; 98 | } 99 | 100 | $refers = (string) $this->refers; 101 | 102 | return $refers . ($description !== '' ? ($refers !== '' ? ' ' : '') . $description : ''); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Since.php: -------------------------------------------------------------------------------- 1 | version = $version; 54 | $this->description = $description; 55 | } 56 | 57 | public static function create( 58 | ?string $body, 59 | ?DescriptionFactory $descriptionFactory = null, 60 | ?TypeContext $context = null 61 | ): ?self { 62 | if ($body === null || $body === '') { 63 | return new static(); 64 | } 65 | 66 | $matches = []; 67 | if (!preg_match('/^(' . self::REGEX_VECTOR . ')\s*(.+)?$/sux', $body, $matches)) { 68 | return null; 69 | } 70 | 71 | Assert::notNull($descriptionFactory); 72 | 73 | return new static( 74 | $matches[1], 75 | $descriptionFactory->create($matches[2] ?? '', $context) 76 | ); 77 | } 78 | 79 | /** 80 | * Gets the version section of the tag. 81 | */ 82 | public function getVersion(): ?string 83 | { 84 | return $this->version; 85 | } 86 | 87 | /** 88 | * Returns a string representation for this tag. 89 | */ 90 | public function __toString(): string 91 | { 92 | if ($this->description !== null) { 93 | $description = $this->description->render(); 94 | } else { 95 | $description = ''; 96 | } 97 | 98 | $version = (string) $this->version; 99 | 100 | return $version . ($description !== '' ? ($version !== '' ? ' ' : '') . $description : ''); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Source.php: -------------------------------------------------------------------------------- 1 | startingLine = (int) $startingLine; 46 | $this->lineCount = $lineCount !== null ? (int) $lineCount : null; 47 | $this->description = $description; 48 | } 49 | 50 | public static function create( 51 | string $body, 52 | ?DescriptionFactory $descriptionFactory = null, 53 | ?TypeContext $context = null 54 | ): self { 55 | Assert::stringNotEmpty($body); 56 | Assert::notNull($descriptionFactory); 57 | 58 | $startingLine = 1; 59 | $lineCount = null; 60 | $description = null; 61 | 62 | // Starting line / Number of lines / Description 63 | if (preg_match('/^([1-9]\d*)\s*(?:((?1))\s+)?(.*)$/sux', $body, $matches)) { 64 | $startingLine = (int) $matches[1]; 65 | if (isset($matches[2]) && $matches[2] !== '') { 66 | $lineCount = (int) $matches[2]; 67 | } 68 | 69 | $description = $matches[3]; 70 | } 71 | 72 | return new static($startingLine, $lineCount, $descriptionFactory->create($description ?? '', $context)); 73 | } 74 | 75 | /** 76 | * Gets the starting line. 77 | * 78 | * @return int The starting line, relative to the structural element's 79 | * location. 80 | */ 81 | public function getStartingLine(): int 82 | { 83 | return $this->startingLine; 84 | } 85 | 86 | /** 87 | * Returns the number of lines. 88 | * 89 | * @return int|null The number of lines, relative to the starting line. NULL 90 | * means "to the end". 91 | */ 92 | public function getLineCount(): ?int 93 | { 94 | return $this->lineCount; 95 | } 96 | 97 | public function __toString(): string 98 | { 99 | if ($this->description) { 100 | $description = $this->description->render(); 101 | } else { 102 | $description = ''; 103 | } 104 | 105 | $startingLine = (string) $this->startingLine; 106 | 107 | $lineCount = $this->lineCount !== null ? ' ' . $this->lineCount : ''; 108 | 109 | return $startingLine 110 | . $lineCount 111 | . ($description !== '' 112 | ? ' ' . $description 113 | : ''); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/TagWithType.php: -------------------------------------------------------------------------------- 1 | type; 36 | } 37 | 38 | /** 39 | * @return string[] 40 | */ 41 | protected static function extractTypeFromBody(string $body): array 42 | { 43 | $type = ''; 44 | $nestingLevel = 0; 45 | for ($i = 0, $iMax = strlen($body); $i < $iMax; $i++) { 46 | $character = $body[$i]; 47 | 48 | if ($nestingLevel === 0 && trim($character) === '') { 49 | break; 50 | } 51 | 52 | $type .= $character; 53 | if (in_array($character, ['<', '(', '[', '{'])) { 54 | $nestingLevel++; 55 | continue; 56 | } 57 | 58 | if (in_array($character, ['>', ')', ']', '}'])) { 59 | $nestingLevel--; 60 | continue; 61 | } 62 | } 63 | 64 | if ($nestingLevel < 0 || $nestingLevel > 0) { 65 | throw new InvalidArgumentException( 66 | sprintf('Could not find type in %s, please check for malformed notations', $body) 67 | ); 68 | } 69 | 70 | $description = trim(substr($body, strlen($type))); 71 | 72 | return [$type, $description]; 73 | } 74 | 75 | public function __toString(): string 76 | { 77 | if ($this->description) { 78 | $description = $this->description->render(); 79 | } else { 80 | $description = ''; 81 | } 82 | 83 | $type = (string) $this->type; 84 | 85 | return $type . ($description !== '' ? ($type !== '' ? ' ' : '') . $description : ''); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Template.php: -------------------------------------------------------------------------------- 1 | name = 'template'; 42 | $this->templateName = $templateName; 43 | $this->bound = $bound; 44 | $this->default = $default; 45 | $this->description = $description; 46 | } 47 | 48 | /** 49 | * @deprecated Create using static factory is deprecated, 50 | * this method should not be called directly by library consumers 51 | */ 52 | public static function create(string $body): ?Tag 53 | { 54 | Deprecation::trigger( 55 | 'phpdocumentor/reflection-docblock', 56 | 'https://github.com/phpDocumentor/ReflectionDocBlock/issues/361', 57 | 'Create using static factory is deprecated, this method should not be called directly 58 | by library consumers', 59 | ); 60 | 61 | return null; 62 | } 63 | 64 | public function getTemplateName(): string 65 | { 66 | return $this->templateName; 67 | } 68 | 69 | public function getBound(): ?Type 70 | { 71 | return $this->bound; 72 | } 73 | 74 | public function getDefault(): ?Type 75 | { 76 | return $this->default; 77 | } 78 | 79 | public function __toString(): string 80 | { 81 | $bound = $this->bound !== null ? ' of ' . $this->bound : ''; 82 | $default = $this->default !== null ? ' = ' . $this->default : ''; 83 | 84 | if ($this->description) { 85 | $description = $this->description->render(); 86 | } else { 87 | $description = ''; 88 | } 89 | 90 | return $this->templateName . $bound . $default . ($description !== '' ? ' ' . $description : ''); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/TemplateCovariant.php: -------------------------------------------------------------------------------- 1 | name = 'template-covariant'; 31 | $this->type = $type; 32 | $this->description = $description; 33 | } 34 | 35 | public static function create( 36 | string $body, 37 | ?TypeResolver $typeResolver = null, 38 | ?DescriptionFactory $descriptionFactory = null, 39 | ?TypeContext $context = null 40 | ): self { 41 | Assert::notNull($typeResolver); 42 | Assert::notNull($descriptionFactory); 43 | 44 | [$type, $description] = self::extractTypeFromBody($body); 45 | 46 | $type = $typeResolver->resolve($type, $context); 47 | $description = $descriptionFactory->create($description, $context); 48 | 49 | return new static($type, $description); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/TemplateExtends.php: -------------------------------------------------------------------------------- 1 | name = 'template-extends'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/TemplateImplements.php: -------------------------------------------------------------------------------- 1 | name = 'template-implements'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Throws.php: -------------------------------------------------------------------------------- 1 | name = 'throws'; 31 | $this->type = $type; 32 | $this->description = $description; 33 | } 34 | 35 | public static function create( 36 | string $body, 37 | ?TypeResolver $typeResolver = null, 38 | ?DescriptionFactory $descriptionFactory = null, 39 | ?TypeContext $context = null 40 | ): self { 41 | Assert::notNull($typeResolver); 42 | Assert::notNull($descriptionFactory); 43 | 44 | [$type, $description] = self::extractTypeFromBody($body); 45 | 46 | $type = $typeResolver->resolve($type, $context); 47 | $description = $descriptionFactory->create($description, $context); 48 | 49 | return new static($type, $description); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Uses.php: -------------------------------------------------------------------------------- 1 | refers = $refers; 42 | $this->description = $description; 43 | } 44 | 45 | public static function create( 46 | string $body, 47 | ?FqsenResolver $resolver = null, 48 | ?DescriptionFactory $descriptionFactory = null, 49 | ?TypeContext $context = null 50 | ): self { 51 | Assert::notNull($resolver); 52 | Assert::notNull($descriptionFactory); 53 | 54 | $parts = Utils::pregSplit('/\s+/Su', $body, 2); 55 | 56 | return new static( 57 | self::resolveFqsen($parts[0], $resolver, $context), 58 | $descriptionFactory->create($parts[1] ?? '', $context) 59 | ); 60 | } 61 | 62 | private static function resolveFqsen(string $parts, ?FqsenResolver $fqsenResolver, ?TypeContext $context): Fqsen 63 | { 64 | Assert::notNull($fqsenResolver); 65 | $fqsenParts = explode('::', $parts); 66 | $resolved = $fqsenResolver->resolve($fqsenParts[0], $context); 67 | 68 | if (!array_key_exists(1, $fqsenParts)) { 69 | return $resolved; 70 | } 71 | 72 | return new Fqsen($resolved . '::' . $fqsenParts[1]); 73 | } 74 | 75 | /** 76 | * Returns the structural element this tag refers to. 77 | */ 78 | public function getReference(): Fqsen 79 | { 80 | return $this->refers; 81 | } 82 | 83 | /** 84 | * Returns a string representation of this tag. 85 | */ 86 | public function __toString(): string 87 | { 88 | if ($this->description) { 89 | $description = $this->description->render(); 90 | } else { 91 | $description = ''; 92 | } 93 | 94 | $refers = (string) $this->refers; 95 | 96 | return $refers . ($description !== '' ? ($refers !== '' ? ' ' : '') . $description : ''); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Var_.php: -------------------------------------------------------------------------------- 1 | name = 'var'; 45 | $this->variableName = $variableName; 46 | $this->type = $type; 47 | $this->description = $description; 48 | } 49 | 50 | /** 51 | * @deprecated Create using static factory is deprecated, 52 | * this method should not be called directly by library consumers 53 | */ 54 | public static function create( 55 | string $body, 56 | ?TypeResolver $typeResolver = null, 57 | ?DescriptionFactory $descriptionFactory = null, 58 | ?TypeContext $context = null 59 | ): self { 60 | Deprecation::triggerIfCalledFromOutside( 61 | 'phpdocumentor/reflection-docblock', 62 | 'https://github.com/phpDocumentor/ReflectionDocBlock/issues/361', 63 | 'Create using static factory is deprecated, this method should not be called directly 64 | by library consumers', 65 | ); 66 | Assert::stringNotEmpty($body); 67 | Assert::notNull($typeResolver); 68 | Assert::notNull($descriptionFactory); 69 | 70 | [$firstPart, $body] = self::extractTypeFromBody($body); 71 | 72 | $parts = Utils::pregSplit('/(\s+)/Su', $body, 2, PREG_SPLIT_DELIM_CAPTURE); 73 | $type = null; 74 | $variableName = ''; 75 | 76 | // if the first item that is encountered is not a variable; it is a type 77 | if ($firstPart && $firstPart[0] !== '$') { 78 | $type = $typeResolver->resolve($firstPart, $context); 79 | } else { 80 | // first part is not a type; we should prepend it to the parts array for further processing 81 | array_unshift($parts, $firstPart); 82 | } 83 | 84 | // if the next item starts with a $ it must be the variable name 85 | if (isset($parts[0]) && strpos($parts[0], '$') === 0) { 86 | $variableName = array_shift($parts); 87 | if ($type) { 88 | array_shift($parts); 89 | } 90 | 91 | Assert::notNull($variableName); 92 | 93 | $variableName = substr($variableName, 1); 94 | } 95 | 96 | $description = $descriptionFactory->create(implode('', $parts), $context); 97 | 98 | return new static($variableName, $type, $description); 99 | } 100 | 101 | /** 102 | * Returns the variable's name. 103 | */ 104 | public function getVariableName(): ?string 105 | { 106 | return $this->variableName; 107 | } 108 | 109 | /** 110 | * Returns a string representation for this tag. 111 | */ 112 | public function __toString(): string 113 | { 114 | if ($this->description !== null) { 115 | $description = $this->description->render(); 116 | } else { 117 | $description = ''; 118 | } 119 | 120 | if ($this->variableName !== null && $this->variableName !== '') { 121 | $variableName = '$' . $this->variableName; 122 | } else { 123 | $variableName = ''; 124 | } 125 | 126 | $type = (string) $this->type; 127 | 128 | return $type 129 | . ($variableName !== '' ? ($type !== '' ? ' ' : '') . $variableName : '') 130 | . ($description !== '' ? ($type !== '' || $variableName !== '' ? ' ' : '') . $description : ''); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/DocBlock/Tags/Version.php: -------------------------------------------------------------------------------- 1 | version = $version; 54 | $this->description = $description; 55 | } 56 | 57 | public static function create( 58 | ?string $body, 59 | ?DescriptionFactory $descriptionFactory = null, 60 | ?TypeContext $context = null 61 | ): ?self { 62 | if ($body === null || $body === '') { 63 | return new static(); 64 | } 65 | 66 | $matches = []; 67 | if (!preg_match('/^(' . self::REGEX_VECTOR . ')\s*(.+)?$/sux', $body, $matches)) { 68 | return null; 69 | } 70 | 71 | $description = null; 72 | if ($descriptionFactory !== null) { 73 | $description = $descriptionFactory->create($matches[2] ?? '', $context); 74 | } 75 | 76 | return new static( 77 | $matches[1], 78 | $description 79 | ); 80 | } 81 | 82 | /** 83 | * Gets the version section of the tag. 84 | */ 85 | public function getVersion(): ?string 86 | { 87 | return $this->version; 88 | } 89 | 90 | /** 91 | * Returns a string representation for this tag. 92 | */ 93 | public function __toString(): string 94 | { 95 | if ($this->description) { 96 | $description = $this->description->render(); 97 | } else { 98 | $description = ''; 99 | } 100 | 101 | $version = (string) $this->version; 102 | 103 | return $version . ($description !== '' ? ($version !== '' ? ' ' : '') . $description : ''); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/DocBlockFactory.php: -------------------------------------------------------------------------------- 1 | descriptionFactory = $descriptionFactory; 62 | $this->tagFactory = $tagFactory; 63 | } 64 | 65 | /** 66 | * Factory method for easy instantiation. 67 | * 68 | * @param array|Factory> $additionalTags 69 | */ 70 | public static function createInstance(array $additionalTags = []): DocBlockFactoryInterface 71 | { 72 | $fqsenResolver = new FqsenResolver(); 73 | $tagFactory = new StandardTagFactory($fqsenResolver); 74 | $descriptionFactory = new DescriptionFactory($tagFactory); 75 | $typeResolver = new TypeResolver($fqsenResolver); 76 | 77 | $phpstanTagFactory = new AbstractPHPStanFactory( 78 | new ParamFactory($typeResolver, $descriptionFactory), 79 | new VarFactory($typeResolver, $descriptionFactory), 80 | new ReturnFactory($typeResolver, $descriptionFactory), 81 | new PropertyFactory($typeResolver, $descriptionFactory), 82 | new PropertyReadFactory($typeResolver, $descriptionFactory), 83 | new PropertyWriteFactory($typeResolver, $descriptionFactory), 84 | new MethodFactory($typeResolver, $descriptionFactory), 85 | new ImplementsFactory($typeResolver, $descriptionFactory), 86 | new ExtendsFactory($typeResolver, $descriptionFactory), 87 | new TemplateFactory($typeResolver, $descriptionFactory), 88 | new TemplateImplementsFactory($typeResolver, $descriptionFactory), 89 | new TemplateExtendsFactory($typeResolver, $descriptionFactory), 90 | ); 91 | 92 | $tagFactory->addService($descriptionFactory); 93 | $tagFactory->addService($typeResolver); 94 | $tagFactory->registerTagHandler('param', $phpstanTagFactory); 95 | $tagFactory->registerTagHandler('var', $phpstanTagFactory); 96 | $tagFactory->registerTagHandler('return', $phpstanTagFactory); 97 | $tagFactory->registerTagHandler('property', $phpstanTagFactory); 98 | $tagFactory->registerTagHandler('property-read', $phpstanTagFactory); 99 | $tagFactory->registerTagHandler('property-write', $phpstanTagFactory); 100 | $tagFactory->registerTagHandler('method', $phpstanTagFactory); 101 | $tagFactory->registerTagHandler('extends', $phpstanTagFactory); 102 | $tagFactory->registerTagHandler('implements', $phpstanTagFactory); 103 | $tagFactory->registerTagHandler('template', $phpstanTagFactory); 104 | $tagFactory->registerTagHandler('template-extends', $phpstanTagFactory); 105 | $tagFactory->registerTagHandler('template-implements', $phpstanTagFactory); 106 | 107 | $docBlockFactory = new self($descriptionFactory, $tagFactory); 108 | foreach ($additionalTags as $tagName => $tagHandler) { 109 | $docBlockFactory->registerTagHandler($tagName, $tagHandler); 110 | } 111 | 112 | return $docBlockFactory; 113 | } 114 | 115 | /** 116 | * @param object|string $docblock A string containing the DocBlock to parse or an object supporting the 117 | * getDocComment method (such as a ReflectionClass object). 118 | */ 119 | public function create($docblock, ?Types\Context $context = null, ?Location $location = null): DocBlock 120 | { 121 | if (is_object($docblock)) { 122 | if (!method_exists($docblock, 'getDocComment')) { 123 | $exceptionMessage = 'Invalid object passed; the given object must support the getDocComment method'; 124 | 125 | throw new InvalidArgumentException($exceptionMessage); 126 | } 127 | 128 | $docblock = $docblock->getDocComment(); 129 | Assert::string($docblock); 130 | } 131 | 132 | Assert::stringNotEmpty($docblock); 133 | 134 | if ($context === null) { 135 | $context = new Types\Context(''); 136 | } 137 | 138 | $parts = $this->splitDocBlock($this->stripDocComment($docblock)); 139 | 140 | [$templateMarker, $summary, $description, $tags] = $parts; 141 | 142 | return new DocBlock( 143 | $summary, 144 | $description ? $this->descriptionFactory->create($description, $context) : null, 145 | $this->parseTagBlock($tags, $context), 146 | $context, 147 | $location, 148 | $templateMarker === '#@+', 149 | $templateMarker === '#@-' 150 | ); 151 | } 152 | 153 | /** 154 | * @param class-string|Factory $handler 155 | */ 156 | public function registerTagHandler(string $tagName, $handler): void 157 | { 158 | $this->tagFactory->registerTagHandler($tagName, $handler); 159 | } 160 | 161 | /** 162 | * Strips the asterisks from the DocBlock comment. 163 | * 164 | * @param string $comment String containing the comment text. 165 | */ 166 | private function stripDocComment(string $comment): string 167 | { 168 | $comment = preg_replace('#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]?(.*)?#u', '$1', $comment); 169 | Assert::string($comment); 170 | $comment = trim($comment); 171 | 172 | // reg ex above is not able to remove */ from a single line docblock 173 | if (substr($comment, -2) === '*/') { 174 | $comment = trim(substr($comment, 0, -2)); 175 | } 176 | 177 | return str_replace(["\r\n", "\r"], "\n", $comment); 178 | } 179 | 180 | // phpcs:disable 181 | 182 | /** 183 | * Splits the DocBlock into a template marker, summary, description and block of tags. 184 | * 185 | * @param string $comment Comment to split into the sub-parts. 186 | * 187 | * @return string[] containing the template marker (if any), summary, description and a string containing the tags. 188 | * 189 | * @author Mike van Riel for extending the regex with template marker support. 190 | * 191 | * @author Richard van Velzen (@_richardJ) Special thanks to Richard for the regex responsible for the split. 192 | */ 193 | private function splitDocBlock(string $comment): array 194 | { 195 | // phpcs:enable 196 | // Performance improvement cheat: if the first character is an @ then only tags are in this DocBlock. This 197 | // method does not split tags so we return this verbatim as the fourth result (tags). This saves us the 198 | // performance impact of running a regular expression 199 | if (strpos($comment, '@') === 0) { 200 | return ['', '', '', $comment]; 201 | } 202 | 203 | // clears all extra horizontal whitespace from the line endings to prevent parsing issues 204 | $comment = preg_replace('/\h*$/Sum', '', $comment); 205 | Assert::string($comment); 206 | /* 207 | * Splits the docblock into a template marker, summary, description and tags section. 208 | * 209 | * - The template marker is empty, #@+ or #@- if the DocBlock starts with either of those (a newline may 210 | * occur after it and will be stripped). 211 | * - The short description is started from the first character until a dot is encountered followed by a 212 | * newline OR two consecutive newlines (horizontal whitespace is taken into account to consider spacing 213 | * errors). This is optional. 214 | * - The long description, any character until a new line is encountered followed by an @ and word 215 | * characters (a tag). This is optional. 216 | * - Tags; the remaining characters 217 | * 218 | * Big thanks to RichardJ for contributing this Regular Expression 219 | */ 220 | preg_match( 221 | '/ 222 | \A 223 | # 1. Extract the template marker 224 | (?:(\#\@\+|\#\@\-)\n?)? 225 | 226 | # 2. Extract the summary 227 | (?: 228 | (?! @\pL ) # The summary may not start with an @ 229 | ( 230 | [^\n.]+ 231 | (?: 232 | (?! \. \n | \n{2} ) # End summary upon a dot followed by newline or two newlines 233 | [\n.]* (?! [ \t]* @\pL ) # End summary when an @ is found as first character on a new line 234 | [^\n.]+ # Include anything else 235 | )* 236 | \.? 237 | )? 238 | ) 239 | 240 | # 3. Extract the description 241 | (?: 242 | \s* # Some form of whitespace _must_ precede a description because a summary must be there 243 | (?! @\pL ) # The description may not start with an @ 244 | ( 245 | [^\n]+ 246 | (?: \n+ 247 | (?! [ \t]* @\pL ) # End description when an @ is found as first character on a new line 248 | [^\n]+ # Include anything else 249 | )* 250 | ) 251 | )? 252 | 253 | # 4. Extract the tags (anything that follows) 254 | (\s+ [\s\S]*)? # everything that follows 255 | /ux', 256 | $comment, 257 | $matches 258 | ); 259 | array_shift($matches); 260 | 261 | while (count($matches) < 4) { 262 | $matches[] = ''; 263 | } 264 | 265 | return $matches; 266 | } 267 | 268 | /** 269 | * Creates the tag objects. 270 | * 271 | * @param string $tags Tag block to parse. 272 | * @param Types\Context $context Context of the parsed Tag 273 | * 274 | * @return DocBlock\Tag[] 275 | */ 276 | private function parseTagBlock(string $tags, Types\Context $context): array 277 | { 278 | $tags = $this->filterTagBlock($tags); 279 | if ($tags === null) { 280 | return []; 281 | } 282 | 283 | $result = []; 284 | $lines = $this->splitTagBlockIntoTagLines($tags); 285 | foreach ($lines as $key => $tagLine) { 286 | $result[$key] = $this->tagFactory->create(trim($tagLine), $context); 287 | } 288 | 289 | return $result; 290 | } 291 | 292 | /** 293 | * @return string[] 294 | */ 295 | private function splitTagBlockIntoTagLines(string $tags): array 296 | { 297 | $result = []; 298 | foreach (explode("\n", $tags) as $tagLine) { 299 | if ($tagLine !== '' && strpos($tagLine, '@') === 0) { 300 | $result[] = $tagLine; 301 | } else { 302 | $result[count($result) - 1] .= "\n" . $tagLine; 303 | } 304 | } 305 | 306 | return $result; 307 | } 308 | 309 | private function filterTagBlock(string $tags): ?string 310 | { 311 | $tags = trim($tags); 312 | if (!$tags) { 313 | return null; 314 | } 315 | 316 | if ($tags[0] !== '@') { 317 | // @codeCoverageIgnoreStart 318 | // Can't simulate this; this only happens if there is an error with the parsing of the DocBlock that 319 | // we didn't foresee. 320 | 321 | throw new LogicException('A tag block started with text instead of an at-sign(@): ' . $tags); 322 | 323 | // @codeCoverageIgnoreEnd 324 | } 325 | 326 | return $tags; 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/DocBlockFactoryInterface.php: -------------------------------------------------------------------------------- 1 | > $additionalTags 16 | */ 17 | public static function createInstance(array $additionalTags = []): self; 18 | 19 | /** 20 | * @param string|object $docblock 21 | */ 22 | public function create($docblock, ?Types\Context $context = null, ?Location $location = null): DocBlock; 23 | } 24 | -------------------------------------------------------------------------------- /src/Exception/PcreException.php: -------------------------------------------------------------------------------- 1 |