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