1<?php 2/** 3 * This file is part of phpDocumentor. 4 * 5 * For the full copyright and license information, please view the LICENSE 6 * file that was distributed with this source code. 7 * 8 * @copyright 2010-2015 Mike van Riel<mike@phpdoc.org> 9 * @license http://www.opensource.org/licenses/mit-license.php MIT 10 * @link http://phpdoc.org 11 */ 12 13namespace phpDocumentor\Reflection; 14 15use phpDocumentor\Reflection\DocBlock\DescriptionFactory; 16use phpDocumentor\Reflection\DocBlock\StandardTagFactory; 17use phpDocumentor\Reflection\DocBlock\Tag; 18use phpDocumentor\Reflection\DocBlock\TagFactory; 19use Webmozart\Assert\Assert; 20 21final class DocBlockFactory implements DocBlockFactoryInterface 22{ 23 /** @var DocBlock\DescriptionFactory */ 24 private $descriptionFactory; 25 26 /** @var DocBlock\TagFactory */ 27 private $tagFactory; 28 29 /** 30 * Initializes this factory with the required subcontractors. 31 * 32 * @param DescriptionFactory $descriptionFactory 33 * @param TagFactory $tagFactory 34 */ 35 public function __construct(DescriptionFactory $descriptionFactory, TagFactory $tagFactory) 36 { 37 $this->descriptionFactory = $descriptionFactory; 38 $this->tagFactory = $tagFactory; 39 } 40 41 /** 42 * Factory method for easy instantiation. 43 * 44 * @param string[] $additionalTags 45 * 46 * @return DocBlockFactory 47 */ 48 public static function createInstance(array $additionalTags = []) 49 { 50 $fqsenResolver = new FqsenResolver(); 51 $tagFactory = new StandardTagFactory($fqsenResolver); 52 $descriptionFactory = new DescriptionFactory($tagFactory); 53 54 $tagFactory->addService($descriptionFactory); 55 $tagFactory->addService(new TypeResolver($fqsenResolver)); 56 57 $docBlockFactory = new self($descriptionFactory, $tagFactory); 58 foreach ($additionalTags as $tagName => $tagHandler) { 59 $docBlockFactory->registerTagHandler($tagName, $tagHandler); 60 } 61 62 return $docBlockFactory; 63 } 64 65 /** 66 * @param object|string $docblock A string containing the DocBlock to parse or an object supporting the 67 * getDocComment method (such as a ReflectionClass object). 68 * @param Types\Context $context 69 * @param Location $location 70 * 71 * @return DocBlock 72 */ 73 public function create($docblock, Types\Context $context = null, Location $location = null) 74 { 75 if (is_object($docblock)) { 76 if (!method_exists($docblock, 'getDocComment')) { 77 $exceptionMessage = 'Invalid object passed; the given object must support the getDocComment method'; 78 throw new \InvalidArgumentException($exceptionMessage); 79 } 80 81 $docblock = $docblock->getDocComment(); 82 } 83 84 Assert::stringNotEmpty($docblock); 85 86 if ($context === null) { 87 $context = new Types\Context(''); 88 } 89 90 $parts = $this->splitDocBlock($this->stripDocComment($docblock)); 91 list($templateMarker, $summary, $description, $tags) = $parts; 92 93 return new DocBlock( 94 $summary, 95 $description ? $this->descriptionFactory->create($description, $context) : null, 96 array_filter($this->parseTagBlock($tags, $context), function ($tag) { 97 return $tag instanceof Tag; 98 }), 99 $context, 100 $location, 101 $templateMarker === '#@+', 102 $templateMarker === '#@-' 103 ); 104 } 105 106 public function registerTagHandler($tagName, $handler) 107 { 108 $this->tagFactory->registerTagHandler($tagName, $handler); 109 } 110 111 /** 112 * Strips the asterisks from the DocBlock comment. 113 * 114 * @param string $comment String containing the comment text. 115 * 116 * @return string 117 */ 118 private function stripDocComment($comment) 119 { 120 $comment = trim(preg_replace('#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]{0,1}(.*)?#u', '$1', $comment)); 121 122 // reg ex above is not able to remove */ from a single line docblock 123 if (substr($comment, -2) === '*/') { 124 $comment = trim(substr($comment, 0, -2)); 125 } 126 127 return str_replace(["\r\n", "\r"], "\n", $comment); 128 } 129 130 /** 131 * Splits the DocBlock into a template marker, summary, description and block of tags. 132 * 133 * @param string $comment Comment to split into the sub-parts. 134 * 135 * @author Richard van Velzen (@_richardJ) Special thanks to Richard for the regex responsible for the split. 136 * @author Mike van Riel <me@mikevanriel.com> for extending the regex with template marker support. 137 * 138 * @return string[] containing the template marker (if any), summary, description and a string containing the tags. 139 */ 140 private function splitDocBlock($comment) 141 { 142 // Performance improvement cheat: if the first character is an @ then only tags are in this DocBlock. This 143 // method does not split tags so we return this verbatim as the fourth result (tags). This saves us the 144 // performance impact of running a regular expression 145 if (strpos($comment, '@') === 0) { 146 return ['', '', '', $comment]; 147 } 148 149 // clears all extra horizontal whitespace from the line endings to prevent parsing issues 150 $comment = preg_replace('/\h*$/Sum', '', $comment); 151 152 /* 153 * Splits the docblock into a template marker, summary, description and tags section. 154 * 155 * - The template marker is empty, #@+ or #@- if the DocBlock starts with either of those (a newline may 156 * occur after it and will be stripped). 157 * - The short description is started from the first character until a dot is encountered followed by a 158 * newline OR two consecutive newlines (horizontal whitespace is taken into account to consider spacing 159 * errors). This is optional. 160 * - The long description, any character until a new line is encountered followed by an @ and word 161 * characters (a tag). This is optional. 162 * - Tags; the remaining characters 163 * 164 * Big thanks to RichardJ for contributing this Regular Expression 165 */ 166 preg_match( 167 '/ 168 \A 169 # 1. Extract the template marker 170 (?:(\#\@\+|\#\@\-)\n?)? 171 172 # 2. Extract the summary 173 (?: 174 (?! @\pL ) # The summary may not start with an @ 175 ( 176 [^\n.]+ 177 (?: 178 (?! \. \n | \n{2} ) # End summary upon a dot followed by newline or two newlines 179 [\n.] (?! [ \t]* @\pL ) # End summary when an @ is found as first character on a new line 180 [^\n.]+ # Include anything else 181 )* 182 \.? 183 )? 184 ) 185 186 # 3. Extract the description 187 (?: 188 \s* # Some form of whitespace _must_ precede a description because a summary must be there 189 (?! @\pL ) # The description may not start with an @ 190 ( 191 [^\n]+ 192 (?: \n+ 193 (?! [ \t]* @\pL ) # End description when an @ is found as first character on a new line 194 [^\n]+ # Include anything else 195 )* 196 ) 197 )? 198 199 # 4. Extract the tags (anything that follows) 200 (\s+ [\s\S]*)? # everything that follows 201 /ux', 202 $comment, 203 $matches 204 ); 205 array_shift($matches); 206 207 while (count($matches) < 4) { 208 $matches[] = ''; 209 } 210 211 return $matches; 212 } 213 214 /** 215 * Creates the tag objects. 216 * 217 * @param string $tags Tag block to parse. 218 * @param Types\Context $context Context of the parsed Tag 219 * 220 * @return DocBlock\Tag[] 221 */ 222 private function parseTagBlock($tags, Types\Context $context) 223 { 224 $tags = $this->filterTagBlock($tags); 225 if (!$tags) { 226 return []; 227 } 228 229 $result = $this->splitTagBlockIntoTagLines($tags); 230 foreach ($result as $key => $tagLine) { 231 $result[$key] = $this->tagFactory->create(trim($tagLine), $context); 232 } 233 234 return $result; 235 } 236 237 /** 238 * @param string $tags 239 * 240 * @return string[] 241 */ 242 private function splitTagBlockIntoTagLines($tags) 243 { 244 $result = []; 245 foreach (explode("\n", $tags) as $tag_line) { 246 if (isset($tag_line[0]) && ($tag_line[0] === '@')) { 247 $result[] = $tag_line; 248 } else { 249 $result[count($result) - 1] .= "\n" . $tag_line; 250 } 251 } 252 253 return $result; 254 } 255 256 /** 257 * @param $tags 258 * @return string 259 */ 260 private function filterTagBlock($tags) 261 { 262 $tags = trim($tags); 263 if (!$tags) { 264 return null; 265 } 266 267 if ('@' !== $tags[0]) { 268 // @codeCoverageIgnoreStart 269 // Can't simulate this; this only happens if there is an error with the parsing of the DocBlock that 270 // we didn't foresee. 271 throw new \LogicException('A tag block started with text instead of an at-sign(@): ' . $tags); 272 // @codeCoverageIgnoreEnd 273 } 274 275 return $tags; 276 } 277} 278