1 | <?php |
||||
2 | /** |
||||
3 | * @copyright Copyright (c) 2017 Julius Härtl <[email protected]> |
||||
4 | * |
||||
5 | * @author Julius Härtl <[email protected]> |
||||
6 | * |
||||
7 | * @license GNU AGPL version 3 or any later version |
||||
8 | * |
||||
9 | * This program is free software: you can redistribute it and/or modify |
||||
10 | * it under the terms of the GNU Affero General Public License as |
||||
11 | * published by the Free Software Foundation, either version 3 of the |
||||
12 | * License, or (at your option) any later version. |
||||
13 | * |
||||
14 | * This program is distributed in the hope that it will be useful, |
||||
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
17 | * GNU Affero General Public License for more details. |
||||
18 | * |
||||
19 | * You should have received a copy of the GNU Affero General Public License |
||||
20 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
21 | * |
||||
22 | */ |
||||
23 | |||||
24 | namespace JuliusHaertl\PHPDocToRst\Builder; |
||||
25 | |||||
26 | use JuliusHaertl\PHPDocToRst\Extension\Extension; |
||||
27 | use phpDocumentor\Reflection\DocBlock; |
||||
28 | use phpDocumentor\Reflection\DocBlock\Tags\Param; |
||||
29 | use phpDocumentor\Reflection\DocBlock\Tags\Return_; |
||||
30 | use phpDocumentor\Reflection\DocBlock\Tags\See; |
||||
31 | use phpDocumentor\Reflection\DocBlock\Tags\Since; |
||||
32 | use phpDocumentor\Reflection\DocBlock\Tags\Throws; |
||||
33 | use phpDocumentor\Reflection\Element; |
||||
34 | use phpDocumentor\Reflection\Php\Argument; |
||||
35 | use phpDocumentor\Reflection\Php\Class_; |
||||
36 | use phpDocumentor\Reflection\Php\Constant; |
||||
37 | use phpDocumentor\Reflection\Php\Function_; |
||||
38 | use phpDocumentor\Reflection\Php\Interface_; |
||||
39 | use phpDocumentor\Reflection\Php\Method; |
||||
40 | use phpDocumentor\Reflection\Php\Property; |
||||
41 | use phpDocumentor\Reflection\DocBlock\Tags\Deprecated; |
||||
42 | use phpDocumentor\Reflection\Php\Trait_; |
||||
43 | |||||
44 | /** |
||||
45 | * Class to build reStructuredText file with sphinxcontrib-phpdomain syntax |
||||
46 | * |
||||
47 | * @package JuliusHaertl\PHPDocToRst\Builder |
||||
48 | */ |
||||
49 | class PhpDomainBuilder extends RstBuilder { |
||||
50 | |||||
51 | const SECTION_BEFORE_DESCRIPTION = self::class . '::SECTION_BEFORE_DESCRIPTION'; |
||||
52 | const SECTION_AFTER_DESCRIPTION = self::class . '::SECTION_AFTER_DESCRIPTION'; |
||||
53 | const SECTION_AFTER_TITLE = self::class . '::SECTION_AFTER_TITLE'; |
||||
54 | const SECTION_AFTER_INTRODUCTION = self::class . '::SECTION_AFTER_INTRODUCTION'; |
||||
55 | |||||
56 | use ExtensionBuilder { |
||||
57 | ExtensionBuilder::__construct as private __extensionConstructor; |
||||
58 | } |
||||
59 | |||||
60 | public function __construct($extensions) { |
||||
61 | $this->__extensionConstructor($extensions); |
||||
62 | $this->addMultiline('.. role:: php(code)' . PHP_EOL . ':language: php', true); |
||||
63 | $this->addLine(); |
||||
64 | } |
||||
65 | |||||
66 | /** |
||||
67 | * Strip element name from Fqsen to return the namespace only |
||||
68 | * |
||||
69 | * @param Element $element |
||||
70 | * @return mixed |
||||
71 | */ |
||||
72 | public static function getNamespace(Element $element) { |
||||
73 | return substr($element->getFqsen(), 0, strlen($element->getFqsen())-strlen('\\'. $element->getName())); |
||||
74 | //return str_replace('\\' . $element->getName(), '', $element->getFqsen()); |
||||
75 | } |
||||
76 | |||||
77 | /** |
||||
78 | * Add namespace |
||||
79 | * @param Element $element |
||||
80 | */ |
||||
81 | protected function addPageHeader(Element $element) { |
||||
82 | $this->addH1(self::escape($element->getName()))->addLine(); |
||||
83 | if (self::getNamespace($element) !== '') { |
||||
84 | $this->beginPhpDomain('namespace', substr(self::getNamespace($element), 1), false); |
||||
85 | } |
||||
86 | if ($element instanceof Class_) { |
||||
87 | $modifiers = $element->isAbstract() ? ' abstract' : ''; |
||||
88 | $modifiers = $element->isFinal() ? ' final' : $modifiers; |
||||
89 | if ($modifiers !== '') { |
||||
90 | $this->addLine('.. rst-class:: ' . $modifiers)->addLine(); |
||||
91 | } |
||||
92 | } |
||||
93 | |||||
94 | $this->callExtensions(self::SECTION_AFTER_TITLE, $element); |
||||
95 | |||||
96 | |||||
97 | $this->beginPhpDomain($this->getTypeForClass($element), $element->getName(), false); |
||||
98 | $this->addLine(); |
||||
99 | } |
||||
100 | |||||
101 | private function getTypeForClass($element) { |
||||
102 | switch (get_class($element)) { |
||||
103 | case Class_::class: |
||||
104 | return 'class'; |
||||
105 | case Interface_::class: |
||||
106 | return 'interface'; |
||||
107 | case Trait_::class: |
||||
108 | return 'trait'; |
||||
109 | case Function_::class: |
||||
110 | return 'function'; |
||||
111 | case Method::class: |
||||
112 | return 'method'; |
||||
113 | default: |
||||
114 | return ''; |
||||
115 | } |
||||
116 | } |
||||
117 | |||||
118 | protected function addAfterIntroduction($element) { |
||||
119 | $this->callExtensions(self::SECTION_AFTER_INTRODUCTION, $element); |
||||
120 | } |
||||
121 | |||||
122 | |||||
123 | protected function addConstants($constants) { |
||||
124 | if (count($constants) > 0) { |
||||
125 | $this->addH2('Constants'); |
||||
126 | foreach ($constants as $constant) { |
||||
127 | if ($this->shouldRenderElement($constant)) { |
||||
128 | $this->addConstant($constant); |
||||
129 | } |
||||
130 | } |
||||
131 | } |
||||
132 | } |
||||
133 | |||||
134 | /** |
||||
135 | * @param Constant $constant |
||||
136 | */ |
||||
137 | private function addConstant(Constant $constant) { |
||||
138 | $this->beginPhpDomain('const', $constant->getName() . ' = ' . self::escape($constant->getValue())); |
||||
139 | $docBlock = $constant->getDocBlock(); |
||||
140 | $this->addDocBlockDescription($constant); |
||||
141 | if ($docBlock) { |
||||
142 | foreach ($docBlock->getTags() as $tag) { |
||||
143 | $this->addDocblockTag($tag->getName(), $docBlock); |
||||
144 | } |
||||
145 | } |
||||
146 | $this->endPhpDomain(); |
||||
147 | } |
||||
148 | |||||
149 | /** |
||||
150 | * @param Property[] $properties |
||||
151 | */ |
||||
152 | protected function addProperties($properties) { |
||||
153 | if (count($properties) > 0) { |
||||
154 | $this->addH2('Properties'); |
||||
155 | foreach ($properties as $property) { |
||||
156 | if ($this->shouldRenderElement($property)) { |
||||
157 | $this->addProperty($property); |
||||
158 | } |
||||
159 | } |
||||
160 | } |
||||
161 | } |
||||
162 | |||||
163 | /** |
||||
164 | * @param Property $property |
||||
165 | */ |
||||
166 | private function addProperty(Property $property) { |
||||
167 | $modifiers = $property->isStatic() ? '' : ' static' ; |
||||
168 | $this->beginPhpDomain('attr', $property->getVisibility() . $modifiers . ' ' . $property->getName()); |
||||
169 | $docBlock = $property->getDocBlock(); |
||||
170 | $this->addDocBlockDescription($property); |
||||
171 | if ($docBlock) { |
||||
172 | foreach ($docBlock->getTags() as $tag) { |
||||
173 | $this->addDocblockTag($tag->getName(), $docBlock); |
||||
174 | } |
||||
175 | } |
||||
176 | $this->endPhpDomain(); |
||||
177 | } |
||||
178 | |||||
179 | /** |
||||
180 | * @param Interface_|Class_ $element |
||||
181 | */ |
||||
182 | protected function addParent($element) { |
||||
183 | if ($element instanceof Class_) { |
||||
184 | $parent = $element->getParent(); |
||||
185 | if ($parent !== null) { |
||||
186 | $this->addFieldList('Parent', $parent !== null ? $this->getLink('class', $parent) : ''); |
||||
187 | } |
||||
188 | } |
||||
189 | if ($element instanceof Interface_) { |
||||
190 | $parents = $element->getParents(); |
||||
191 | foreach ($parents as $parent) { |
||||
192 | $this->addFieldList('Parent', $parent !== null ? $this->getLink('interface', $parent) : ''); |
||||
193 | } |
||||
194 | } |
||||
195 | } |
||||
196 | |||||
197 | /** |
||||
198 | * @param Class_|Trait_ $element |
||||
199 | */ |
||||
200 | protected function addUsedTraits($element) { |
||||
201 | $usedTraits = ''; |
||||
202 | foreach ($element->getUsedTraits() as $trait) { |
||||
203 | $usedTraits .= $this->getLink('trait', $trait) . ' '; |
||||
204 | } |
||||
205 | if ($usedTraits !== '') { |
||||
206 | $this->addFieldList('Used traits', $usedTraits); |
||||
207 | } |
||||
208 | } |
||||
209 | |||||
210 | /** |
||||
211 | * @param $methods |
||||
212 | */ |
||||
213 | protected function addMethods($methods) { |
||||
214 | if (count($methods) > 0) { |
||||
215 | $this->addH2('Methods'); |
||||
216 | foreach ($methods as $method) { |
||||
217 | $this->addMethod($method); |
||||
218 | } |
||||
219 | } |
||||
220 | } |
||||
221 | |||||
222 | private function addMethod(Method $method) { |
||||
223 | if (!$this->shouldRenderElement($method)) { |
||||
224 | return; |
||||
225 | } |
||||
226 | $docBlock = $method->getDocBlock(); |
||||
227 | $params = []; |
||||
228 | $deprecated = []; |
||||
229 | if ($docBlock !== null) { |
||||
230 | /** @var Param $param */ |
||||
231 | foreach ($docBlock->getTagsByName('param') as $param) { |
||||
232 | $params[$param->getVariableName()] = $param; |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
233 | } |
||||
234 | $deprecated = $docBlock->getTagsByName('deprecated'); |
||||
235 | } |
||||
236 | $args = ''; |
||||
237 | /** @var Argument $argument */ |
||||
238 | foreach ($method->getArguments() as $argument) { |
||||
239 | // This will work after https://github.com/phpDocumentor/Reflection/pull/109 is merged |
||||
240 | foreach ($argument->getTypes() as $type) { |
||||
241 | $args .= self::escape($type) . '|'; |
||||
242 | } |
||||
243 | $args = substr($args, 0, -1) . ' '; |
||||
244 | if($argument->isVariadic()) { |
||||
245 | $args .= '...'; |
||||
246 | } |
||||
247 | if($argument->isByReference()) { |
||||
248 | $args .= '&'; |
||||
249 | } |
||||
250 | $args .= '$' . $argument->getName(); |
||||
251 | $default = $argument->getDefault(); |
||||
252 | if ($default !== null) { |
||||
253 | $default = $default === '' ? '""' : $default; |
||||
254 | $args .= '=' . self::escape($default); |
||||
255 | } |
||||
256 | $args .= ', '; |
||||
257 | } |
||||
258 | $args = substr($args, 0, -2); |
||||
259 | |||||
260 | $modifiers = $method->getVisibility(); |
||||
261 | $modifiers .= $method->isAbstract() ? ' abstract' : ''; |
||||
262 | $modifiers .= $method->isFinal() ? ' final' : ''; |
||||
263 | $modifiers .= $method->isStatic() ? ' static' : ''; |
||||
264 | $deprecated = count($deprecated) > 0 ? ' deprecated' : ''; |
||||
265 | $this->addLine('.. rst-class:: ' . $modifiers . $deprecated)->addLine(); |
||||
266 | $this->indent(); |
||||
267 | $this->beginPhpDomain('method', $modifiers . ' ' . $method->getName() . '(' . $args . ')'); |
||||
268 | $this->addDocBlockDescription($method); |
||||
269 | $this->addLine(); |
||||
270 | if (!empty($params)) { |
||||
271 | $parameterDetails = ''; |
||||
272 | foreach ($method->getArguments() as $argument) { |
||||
273 | if (!array_key_exists($argument->getName(), $params)) { |
||||
274 | continue; |
||||
275 | } |
||||
276 | /** @var Param $param */ |
||||
277 | $param = $params[$argument->getName()]; |
||||
278 | if ($param !== null) { |
||||
279 | $typString = $param->getType(); |
||||
280 | // Remove first \ to allow references |
||||
281 | if (0 === strpos($typString, '\\')) { |
||||
282 | $typString = substr($typString, 1); |
||||
283 | } |
||||
284 | $paramItem = '* '; |
||||
285 | $paramItem .= '**$' . $argument->getName() . '** '; |
||||
286 | if ($typString !== null) { |
||||
287 | $paramItem .= '(' . self::typesToRst($typString) . ') '; |
||||
288 | } |
||||
289 | $paramItem .= ' ' . $param->getDescription(); |
||||
290 | $parameterDetails .= $paramItem . PHP_EOL; |
||||
291 | } |
||||
292 | } |
||||
293 | $this->addFieldList('Parameters', $parameterDetails); |
||||
294 | } |
||||
295 | if ($docBlock !== null) { |
||||
296 | foreach ($docBlock->getTags() as $tag) { |
||||
297 | $this->addDocblockTag($tag->getName(), $docBlock); |
||||
298 | } |
||||
299 | } |
||||
300 | $this->endPhpDomain('method'); |
||||
301 | $this->unindent(); |
||||
302 | } |
||||
303 | |||||
304 | /** |
||||
305 | * @param $type string |
||||
306 | * @param $fqsen string |
||||
307 | * @return string |
||||
308 | */ |
||||
309 | public static function getLink($type, $fqsen, $description='') { |
||||
310 | if($description !== '') { |
||||
311 | return ':php:' . $type . ':`' . RstBuilder::escape($description) . '<' . RstBuilder::escape(substr($fqsen, 1)) . '>`'; |
||||
312 | } |
||||
313 | return ':php:' . $type . ':`' . RstBuilder::escape(substr($fqsen, 1)) . '`'; |
||||
314 | } |
||||
315 | |||||
316 | /** |
||||
317 | * @param $type string |
||||
318 | * @param $name string |
||||
319 | * @param $indent bool Should indent after the section started |
||||
320 | */ |
||||
321 | public function beginPhpDomain($type, $name, $indent = true) { |
||||
322 | // FIXME: Add checks if it is properly ended |
||||
323 | $this->addLine('.. php:' . $type . ':: ' . $name)->addLine(); |
||||
324 | if ($indent === true) { |
||||
325 | $this->indent(); |
||||
326 | } |
||||
327 | } |
||||
328 | |||||
329 | /** |
||||
330 | * @param string $type |
||||
331 | * @return $this |
||||
332 | */ |
||||
333 | public function endPhpDomain($type = '') { |
||||
0 ignored issues
–
show
The parameter
$type is not used and could be removed.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for parameters that have been defined for a function or method, but which are not used in the method body. ![]() |
|||||
334 | $this->unindent(); |
||||
335 | $this->addLine(); |
||||
336 | } |
||||
337 | |||||
338 | /** |
||||
339 | * @param Class_|Interface_|Trait_|Property|Method|Constant $element |
||||
340 | * @return $this |
||||
341 | */ |
||||
342 | public function addDocBlockDescription($element) { |
||||
343 | if ($element === null) { |
||||
344 | return $this; |
||||
345 | } |
||||
346 | $docBlock = $element->getDocBlock(); |
||||
347 | $this->callExtensions(self::SECTION_BEFORE_DESCRIPTION, $element); |
||||
348 | if ($docBlock !== null && $docBlock->getSummary() !== '') { |
||||
349 | $this->addLine('.. rst-class:: phpdoc-description')->addLine(); |
||||
350 | $this->indent(); |
||||
351 | $this->addMultilineWithoutRendering(RstBuilder::escape($docBlock->getSummary()))->addLine(); |
||||
352 | if ((string)$docBlock->getDescription() !== '') { |
||||
353 | $this->addMultilineWithoutRendering(RstBuilder::escape($docBlock->getDescription()))->addLine(); |
||||
354 | } |
||||
355 | $this->unindent(); |
||||
356 | } |
||||
357 | $this->callExtensions(self::SECTION_AFTER_DESCRIPTION, $element); |
||||
358 | return $this; |
||||
359 | } |
||||
360 | |||||
361 | /** |
||||
362 | * @param string $tagName Name of the tag to parse |
||||
363 | * @param DocBlock $docBlock |
||||
364 | */ |
||||
365 | protected function addDocblockTag($tagName, DocBlock $docBlock) { |
||||
366 | $tags = $docBlock->getTagsByName($tagName); |
||||
367 | switch ($tagName) { |
||||
368 | case 'return': |
||||
369 | if (count($tags) === 0) continue; |
||||
370 | /** @var Return_ $return */ |
||||
371 | $return = $tags[0]; |
||||
372 | $this->addMultiline(':Returns: ' . self::typesToRst($return->getType()) . ' ' . RstBuilder::escape($return->getDescription()), true); |
||||
373 | break; |
||||
374 | case 'var': |
||||
375 | if (count($tags) === 0) continue; |
||||
376 | /** @var DocBlock\Tags\Var_ $return */ |
||||
377 | $return = $tags[0]; |
||||
378 | $this->addMultiline(':Type: ' . self::typesToRst($return->getType()) . ' ' . RstBuilder::escape($return->getDescription()), true); |
||||
379 | break; |
||||
380 | case 'throws': |
||||
381 | if (count($tags) === 0) continue; |
||||
382 | /** @var Throws $tag */ |
||||
383 | foreach ($tags as $tag) { |
||||
384 | $this->addMultiline(':Throws: ' . self::typesToRst($tag->getType()) . ' ' . RstBuilder::escape($tag->getDescription()), true); |
||||
0 ignored issues
–
show
The method
getType() does not exist on phpDocumentor\Reflection\DocBlock\Tag . It seems like you code against a sub-type of phpDocumentor\Reflection\DocBlock\Tag such as phpDocumentor\Reflection\DocBlock\Tags\Property or phpDocumentor\Reflection\DocBlock\Tags\Var_ or phpDocumentor\Reflection\DocBlock\Tags\Param or phpDocumentor\Reflection...lock\Tags\PropertyWrite or phpDocumentor\Reflection\DocBlock\Tags\Return_ or phpDocumentor\Reflection\DocBlock\Tags\Throws or phpDocumentor\Reflection...Block\Tags\PropertyRead .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
385 | } |
||||
386 | break; |
||||
387 | case 'since': |
||||
388 | if (count($tags) === 0) continue; |
||||
389 | /** @var Since $return */ |
||||
390 | $return = $tags[0]; |
||||
391 | $this->addMultiline(':Since: ' . $return->getVersion() . ' ' . RstBuilder::escape($return->getDescription()), true); |
||||
392 | break; |
||||
393 | case 'deprecated': |
||||
394 | if (count($tags) === 0) continue; |
||||
395 | /** @var Deprecated $return */ |
||||
396 | $return = $tags[0]; |
||||
397 | $this->addMultiline(':Deprecated: ' . $return->getVersion() . ' ' . RstBuilder::escape($return->getDescription()), true); |
||||
398 | break; |
||||
399 | case 'see': |
||||
400 | if (count($tags) === 0) continue; |
||||
401 | /** @var See $return */ |
||||
402 | $return = $tags[0]; |
||||
403 | $this->addMultiline(':See: ' . self::typesToRst($return->getReference()) . ' ' . RstBuilder::escape($return->getDescription()), true); |
||||
404 | break; |
||||
405 | case 'license': |
||||
406 | if (count($tags) === 0) continue; |
||||
407 | /** @var DocBlock\Tags\BaseTag $return */ |
||||
408 | $return = $tags[0]; |
||||
409 | $this->addMultiline(':License: ' . RstBuilder::escape($return->getDescription()), true); |
||||
410 | break; |
||||
411 | case 'param': |
||||
412 | // param handling is done by subclasses since it is more that docbook parsing |
||||
413 | break; |
||||
414 | default: |
||||
415 | //echo 'Tag handling not defined for: ' . $tag . PHP_EOL; |
||||
416 | break; |
||||
417 | } |
||||
418 | |||||
419 | } |
||||
420 | |||||
421 | /** |
||||
422 | * @param string $typesString |
||||
423 | * @return bool|string |
||||
424 | */ |
||||
425 | public static function typesToRst($typesString) { |
||||
426 | // http://docs.phpdoc.org/guides/types.html |
||||
427 | $whitelist = [ |
||||
428 | 'string', 'int', 'integer', 'float', 'bool', 'boolean', 'array', 'resource', 'null', 'callable', |
||||
429 | 'mixed', 'void', 'object', 'false', 'true', 'self', 'static', '$this' |
||||
430 | ]; |
||||
431 | $types = explode('|', $typesString); |
||||
432 | $result = ''; |
||||
433 | /** @var string $type */ |
||||
434 | foreach ($types as $typeFull) { |
||||
435 | $type = str_replace('[]', '', $typeFull); |
||||
436 | if (in_array($type, $whitelist, true)) { |
||||
437 | $result .= $typeFull . ' | '; |
||||
438 | continue; |
||||
439 | } |
||||
440 | if (0 === strpos($type, '\\')) |
||||
441 | $type = substr($type, 1); |
||||
442 | $result .= ':any:`' . RstBuilder::escape($typeFull) . ' <' . RstBuilder::escape($type) . '>` | '; |
||||
443 | } |
||||
444 | return substr($result, 0, -3); |
||||
445 | } |
||||
446 | |||||
447 | /** |
||||
448 | * @param Element $element |
||||
449 | * @return bool |
||||
450 | */ |
||||
451 | public function shouldRenderElement(Element $element) { |
||||
452 | /** @var Extension $extension */ |
||||
453 | foreach ($this->extensions as $extension) { |
||||
454 | if ($extension->shouldRenderElement($element) === false) { |
||||
455 | return false; |
||||
456 | } |
||||
457 | } |
||||
458 | return true; |
||||
459 | } |
||||
460 | |||||
461 | |||||
462 | } |