PhpDomainBuilder::addAfterIntroduction()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
/**
3
 * @copyright Copyright (c) 2017 Julius Härtl <[email protected]>
4
 * @author    Julius Härtl <[email protected]>
5
 * @license   GNU AGPL version 3 or any later version
6
 *
7
 *  This program is free software: you can redistribute it and/or modify
8
 *  it under the terms of the GNU Affero General Public License as
9
 *  published by the Free Software Foundation, either version 3 of the
10
 *  License, or (at your option) any later version.
11
 *
12
 *  This program is distributed in the hope that it will be useful,
13
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 *  GNU Affero General Public License for more details.
16
 *
17
 *  You should have received a copy of the GNU Affero General Public License
18
 *  along with this program. If not, see <http://www.gnu.org/licenses/>.
19
 */
20
21
namespace JuliusHaertl\PHPDocToRst\Builder;
22
23
use JuliusHaertl\PHPDocToRst\Extension\Extension;
24
use phpDocumentor\Reflection\DocBlock;
25
use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;
26
use phpDocumentor\Reflection\DocBlock\Tags\Param;
27
use phpDocumentor\Reflection\DocBlock\Tags\Return_;
28
use phpDocumentor\Reflection\DocBlock\Tags\See;
29
use phpDocumentor\Reflection\DocBlock\Tags\Since;
30
use phpDocumentor\Reflection\DocBlock\Tags\Throws;
31
use phpDocumentor\Reflection\Element;
32
use phpDocumentor\Reflection\Php\Argument;
33
use phpDocumentor\Reflection\Php\Class_;
34
use phpDocumentor\Reflection\Php\Constant;
35
use phpDocumentor\Reflection\Php\Function_;
36
use phpDocumentor\Reflection\Php\Interface_;
37
use phpDocumentor\Reflection\Php\Method;
38
use phpDocumentor\Reflection\Php\Property;
39
use phpDocumentor\Reflection\Php\Trait_;
40
41
/**
42
 * Class to build reStructuredText file with sphinxcontrib-phpdomain syntax.
43
 */
44
class PhpDomainBuilder extends RstBuilder
45
{
46
    const SECTION_BEFORE_DESCRIPTION = self::class.'::SECTION_BEFORE_DESCRIPTION';
47
    const SECTION_AFTER_DESCRIPTION = self::class.'::SECTION_AFTER_DESCRIPTION';
48
    const SECTION_AFTER_TITLE = self::class.'::SECTION_AFTER_TITLE';
49
    const SECTION_AFTER_INTRODUCTION = self::class.'::SECTION_AFTER_INTRODUCTION';
50
51
    use ExtensionBuilder {
52
        ExtensionBuilder::__construct as private __extensionConstructor;
53
    }
54
55
    public function __construct($extensions)
56
    {
57
        $this->__extensionConstructor($extensions);
58
        $this->addMultiline('.. role:: php(code)'.PHP_EOL.':language: php', true);
59
        $this->addLine();
60
    }
61
62
    /**
63
     * Add namespace.
64
     *
65
     * @param Element $element
66
     */
67
    protected function addPageHeader(Element $element)
68
    {
69
        $this->addH1(self::escape($element->getName()))->addLine();
70
        if (self::getNamespace($element) !== '') {
71
            $this->beginPhpDomain('namespace', substr(self::getNamespace($element), 1), false);
72
        }
73
        if ($element instanceof Class_) {
74
            $modifiers = $element->isAbstract() ? ' abstract' : '';
75
            $modifiers = $element->isFinal() ? ' final' : $modifiers;
76
            if ($modifiers !== '') {
77
                $this->addLine('.. rst-class:: '.$modifiers)->addLine();
78
            }
79
        }
80
81
        $this->callExtensions(self::SECTION_AFTER_TITLE, $element);
82
83
        $this->beginPhpDomain($this->getTypeForClass($element), $element->getName(), false);
84
        $this->addLine();
85
    }
86
87
    /**
88
     * Strip element name from Fqsen to return the namespace only.
89
     *
90
     * @param Element $element
91
     *
92
     * @return mixed
93
     */
94
    public static function getNamespace(Element $element)
95
    {
96
        return substr($element->getFqsen(), 0, strlen($element->getFqsen()) - strlen('\\'.$element->getName()));
97
        //return str_replace('\\' . $element->getName(), '', $element->getFqsen());
98
    }
99
100
    /**
101
     * @param $type   string
102
     * @param $name   string
103
     * @param $indent bool Should indent after the section started
104
     */
105
    public function beginPhpDomain($type, $name, $indent = true)
106
    {
107
        // FIXME: Add checks if it is properly ended
108
        $this->addLine('.. php:'.$type.':: '.$name)->addLine();
109
        if ($indent === true) {
110
            $this->indent();
111
        }
112
    }
113
114
    private function getTypeForClass($element)
115
    {
116
        switch (get_class($element)) {
117
            case Class_::class:
118
                return 'class';
119
            case Interface_::class:
120
                return 'interface';
121
            case Trait_::class:
122
                return 'trait';
123
            case Function_::class:
124
                return 'function';
125
            case Method::class:
126
                return 'method';
127
            default:
128
                return '';
129
        }
130
    }
131
132
    protected function addAfterIntroduction($element)
133
    {
134
        $this->callExtensions(self::SECTION_AFTER_INTRODUCTION, $element);
135
    }
136
137
    protected function addConstants($constants)
138
    {
139
        if (count($constants) > 0) {
140
            $this->addH2('Constants');
141
            foreach ($constants as $constant) {
142
                if ($this->shouldRenderElement($constant)) {
143
                    $this->addConstant($constant);
144
                }
145
            }
146
        }
147
    }
148
149
    /**
150
     * @param Element $element
151
     *
152
     * @return bool
153
     */
154
    public function shouldRenderElement(Element $element)
155
    {
156
        /** @var Extension $extension */
157
        foreach ($this->extensions as $extension) {
158
            if ($extension->shouldRenderElement($element) === false) {
159
                return false;
160
            }
161
        }
162
163
        return true;
164
    }
165
166
    /**
167
     * @param Constant $constant
168
     */
169
    private function addConstant(Constant $constant)
170
    {
171
        $this->beginPhpDomain('const', $constant->getName().' = '.self::escape($constant->getValue()));
172
        $docBlock = $constant->getDocBlock();
173
        $this->addDocBlockDescription($constant);
174
        if ($docBlock) {
175
            foreach ($docBlock->getTags() as $tag) {
176
                $this->addDocblockTag($tag->getName(), $docBlock);
177
            }
178
        }
179
        $this->endPhpDomain();
180
    }
181
182
    /**
183
     * @param Class_|Interface_|Trait_|Property|Method|Constant $element
184
     *
185
     * @return $this
186
     */
187
    public function addDocBlockDescription($element)
188
    {
189
        if ($element === null) {
190
            return $this;
191
        }
192
        $docBlock = $element->getDocBlock();
193
        $this->callExtensions(self::SECTION_BEFORE_DESCRIPTION, $element);
194
        if ($docBlock !== null && $docBlock->getSummary() !== '') {
195
            $this->addLine('.. rst-class:: phpdoc-description')->addLine();
196
            $this->indent();
197
            $this->addMultilineWithoutRendering(RstBuilder::escape($docBlock->getSummary()))->addLine();
198
            if ((string) $docBlock->getDescription() !== '') {
199
                $this->addMultilineWithoutRendering(RstBuilder::escape($docBlock->getDescription()))->addLine();
200
            }
201
            $this->unindent();
202
        }
203
        $this->callExtensions(self::SECTION_AFTER_DESCRIPTION, $element);
204
205
        return $this;
206
    }
207
208
    /**
209
     * @param string   $tagName  Name of the tag to parse
210
     * @param DocBlock $docBlock
211
     */
212
    protected function addDocblockTag($tagName, DocBlock $docBlock)
213
    {
214
        $inclusion_tag_name = [
215
            'return',
216
            'var',
217
            'throws',
218
            'since',
219
            'deprecated',
220
            'see',
221
            'license',
222
        ];
223
224
        $tags = $docBlock->getTagsByName($tagName);
225
226
        if (!in_array($tagName, $inclusion_tag_name) || (in_array($tagName, $inclusion_tag_name) && count($tags) === 0)) {
227
            return;
228
        }
229
230
        switch ($tagName) {
231
            case 'return':
232
                /** @var Return_ $return */
233
                $return = $tags[0];
234
                $this->addMultiline(
235
                    ':Returns: '.self::typesToRst($return->getType()).' '.RstBuilder::escape($return->getDescription()),
236
                    true
237
                );
238
                break;
239
            case 'var':
240
                /** @var DocBlock\Tags\Var_ $return */
241
                $return = $tags[0];
242
                $this->addMultiline(
243
                    ':Type: '.self::typesToRst($return->getType()).' '.RstBuilder::escape($return->getDescription()),
244
                    true
245
                );
246
                break;
247
            case 'throws':
248
                /** @var Throws $tag */
249
                foreach ($tags as $tag) {
250
                    $this->addMultiline(
251
                        ':Throws: '.self::typesToRst($tag->getType()).' '.RstBuilder::escape($tag->getDescription()),
252
                        true
253
                    );
254
                }
255
                break;
256
            case 'since':
257
                /** @var Since $return */
258
                $return = $tags[0];
259
                $this->addMultiline(
260
                    ':Since: '.$return->getVersion().' '.RstBuilder::escape($return->getDescription()),
261
                    true
262
                );
263
                break;
264
            case 'deprecated':
265
                /** @var Deprecated $return */
266
                $return = $tags[0];
267
                $this->addMultiline(
268
                    ':Deprecated: '.$return->getVersion().' '.RstBuilder::escape($return->getDescription()),
269
                    true
270
                );
271
                break;
272
            case 'see':
273
                /** @var See $return */
274
                $return = $tags[0];
275
                $this->addMultiline(
276
                    ':See: '.self::typesToRst($return->getReference()).' '.RstBuilder::escape($return->getDescription()),
277
                    true
278
                );
279
                break;
280
            case 'license':
281
                /** @var DocBlock\Tags\BaseTag $return */
282
                $return = $tags[0];
283
                $this->addMultiline(':License: '.RstBuilder::escape($return->getDescription()), true);
284
                break;
285
        }
286
    }
287
288
    /**
289
     * @param string $typesString
290
     *
291
     * @return bool|string
292
     */
293
    public static function typesToRst($typesString)
294
    {
295
        // http://docs.phpdoc.org/guides/types.html
296
        $whitelist = [
297
            'string',
298
            'int',
299
            'integer',
300
            'float',
301
            'bool',
302
            'boolean',
303
            'array',
304
            'resource',
305
            'null',
306
            'callable',
307
            'mixed',
308
            'void',
309
            'object',
310
            'false',
311
            'true',
312
            'self',
313
            'static',
314
            '$this',
315
        ];
316
        $types = explode('|', $typesString);
317
        $result = '';
318
        /* @var string $type */
319
        foreach ($types as $typeFull) {
320
            $type = str_replace('[]', '', $typeFull);
321
            if (in_array($type, $whitelist, true)) {
322
                $result .= $typeFull.' | ';
323
                continue;
324
            }
325
            if (0 === strpos($type, '\\')) {
326
                $type = substr($type, 1);
327
            }
328
            $result .= ':any:`'.RstBuilder::escape($typeFull).' <'.RstBuilder::escape($type).'>` | ';
329
        }
330
331
        return substr($result, 0, -3);
332
    }
333
334
    /**
335
     * @param string $type
336
     *
337
     * @return $this
338
     */
339
    public function endPhpDomain($type = '')
0 ignored issues
show
Unused Code introduced by
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 ignore-unused  annotation

339
    public function endPhpDomain(/** @scrutinizer ignore-unused */ $type = '')

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
340
    {
341
        $this->unindent();
342
        $this->addLine();
343
    }
344
345
    /**
346
     * @param Property[] $properties
347
     */
348
    protected function addProperties($properties)
349
    {
350
        //Choose display properties
351
        $displayProperties = [];
352
        foreach ($properties as $property) {
353
            if ($this->shouldRenderElement($property)) {
354
                $displayProperties[] = $property;
355
            }
356
        }
357
358
        //Render
359
        if (count($displayProperties) > 0) {
360
            $this->addH2('Properties');
361
            foreach ($displayProperties as $property) {
362
                $this->addProperty($property);
363
            }
364
        }
365
    }
366
367
    /**
368
     * @param Property $property
369
     */
370
    private function addProperty(Property $property)
371
    {
372
        $modifiers = $property->isStatic() ? '' : ' static';
373
        $this->beginPhpDomain('attr', $property->getVisibility().$modifiers.' '.$property->getName());
374
        $docBlock = $property->getDocBlock();
375
        $this->addDocBlockDescription($property);
376
        if ($docBlock) {
377
            foreach ($docBlock->getTags() as $tag) {
378
                $this->addDocblockTag($tag->getName(), $docBlock);
379
            }
380
        }
381
        $this->endPhpDomain();
382
    }
383
384
    /**
385
     * @param Interface_|Class_ $element
386
     */
387
    protected function addParent($element)
388
    {
389
        if ($element instanceof Class_) {
390
            $parent = $element->getParent();
391
            if ($parent !== null) {
392
                $this->addFieldList('Parent', $parent !== null ? $this->getLink('class', $parent) : '');
393
            }
394
        }
395
        if ($element instanceof Interface_) {
396
            $parents = $element->getParents();
397
            foreach ($parents as $parent) {
398
                $this->addFieldList('Parent', $parent !== null ? $this->getLink('interface', $parent) : '');
399
            }
400
        }
401
    }
402
403
    /**
404
     * @param $type  string
405
     * @param $fqsen string
406
     *
407
     * @return string
408
     */
409
    public static function getLink($type, $fqsen, $description = '')
410
    {
411
        if ($description !== '') {
412
            return ':php:'.$type.':`'.RstBuilder::escape($description).'<'.RstBuilder::escape(substr(
413
                $fqsen,
414
                1
415
            )).'>`';
416
        }
417
418
        return ':php:'.$type.':`'.RstBuilder::escape(substr($fqsen, 1)).'`';
419
    }
420
421
    /**
422
     * @param Class_|Trait_ $element
423
     */
424
    protected function addUsedTraits($element)
425
    {
426
        $usedTraits = '';
427
        foreach ($element->getUsedTraits() as $trait) {
428
            $usedTraits .= $this->getLink('trait', $trait).' ';
429
        }
430
        if ($usedTraits !== '') {
431
            $this->addFieldList('Used traits', $usedTraits);
432
        }
433
    }
434
435
    /**
436
     * @param $methods
437
     */
438
    protected function addMethods($methods)
439
    {
440
        if (count($methods) > 0) {
441
            $this->addH2('Methods');
442
            foreach ($methods as $method) {
443
                $this->addMethod($method);
444
            }
445
        }
446
    }
447
448
    private function addMethod(Method $method)
449
    {
450
        if (!$this->shouldRenderElement($method)) {
451
            return;
452
        }
453
        $docBlock = $method->getDocBlock();
454
        $params = [];
455
        $deprecated = [];
456
        if ($docBlock !== null) {
457
            /** @var Param $param */
458
            foreach ($docBlock->getTagsByName('param') as $param) {
459
                if ($param instanceof Param) {
460
                    $params[$param->getVariableName()] = $param;
461
                }
462
            }
463
            $deprecated = $docBlock->getTagsByName('deprecated');
464
        }
465
466
        $modifiers = $method->getVisibility();
467
        $modifiers .= $method->isAbstract() ? ' abstract' : '';
468
        $modifiers .= $method->isFinal() ? ' final' : '';
469
        $modifiers .= $method->isStatic() ? ' static' : '';
470
        $deprecated = count($deprecated) > 0 ? ' deprecated' : '';
471
        $this->addLine('.. rst-class:: '.$modifiers.$deprecated)->addLine();
472
        $this->indent();
473
474
        $args = $this->processMethodArgumentTypes($method);
475
        $this->beginPhpDomain('method', $modifiers.' '.$method->getName().'('.$args.')');
476
        $this->addDocBlockDescription($method);
477
        $this->addLine();
478
        if (!empty($params)) {
479
            $parameterDetails = $this->processMethodArgumentDocs($method, $params);
480
            $this->addFieldList('Parameters', $parameterDetails);
481
        }
482
        if ($docBlock !== null) {
483
            foreach ($docBlock->getTags() as $tag) {
484
                $this->addDocblockTag($tag->getName(), $docBlock);
485
            }
486
        }
487
        $this->endPhpDomain('method');
488
        $this->unindent();
489
    }
490
491
    /**
492
     * @param Method $method
493
     * @param array  $params
494
     *
495
     * @return string
496
     */
497
    private function processMethodArgumentDocs(Method $method, array $params): string
498
    {
499
        $parameterDetails = '';
500
        foreach ($method->getArguments() as $argument) {
501
            if (!array_key_exists($argument->getName(), $params)) {
502
                continue;
503
            }
504
            /** @var Param $param */
505
            $param = $params[$argument->getName()];
506
            if ($param !== null) {
507
                $typString = $param->getType();
508
                // Remove first \ to allow references
509
                if (0 === strpos($typString, '\\')) {
510
                    $typString = substr($typString, 1);
511
                }
512
                $paramItem = '* ';
513
                $paramItem .= '**';
514
                if ($argument->isVariadic()) {
515
                    $paramItem .= '...';
516
                }
517
                $paramItem .= '$'.$argument->getName().'** ';
518
                if ($typString !== null) {
519
                    $paramItem .= '('.self::typesToRst($typString).') ';
520
                }
521
                $paramItem .= ' '.$param->getDescription();
522
                $parameterDetails .= $paramItem.PHP_EOL;
523
            }
524
        }
525
526
        return $parameterDetails;
527
    }
528
529
    /**
530
     * @param Method $method
531
     *
532
     * @return string
533
     */
534
    private function processMethodArgumentTypes(Method $method): string
535
    {
536
        $args = '';
537
        /** @var Argument $argument */
538
        foreach ($method->getArguments() as $argument) {
539
            $args = $this->processMethodArgumentType($argument, $args);
540
        }
541
542
        return substr($args, 0, -2);
543
    }
544
545
    /**
546
     * @param Argument $argument
547
     * @param string   $args
548
     *
549
     * @return string
550
     */
551
    private function processMethodArgumentType(Argument $argument, string $args): string
552
    {
553
        foreach ($argument->getType() as $type) {
554
            $args .= self::escape($type).'|';
555
        }
556
        $args = substr($args, 0, -1).' ';
557
        if ($argument->isVariadic()) {
558
            $args .= '...';
559
        }
560
        if ($argument->isByReference()) {
561
            $args .= '&';
562
        }
563
        $args .= '$'.$argument->getName();
564
        $default = $argument->getDefault();
565
        if ($default !== null) {
566
            $default = $default === '' ? '""' : $default;
567
            $args .= '='.self::escape($default);
568
        }
569
        $args .= ', ';
570
571
        return $args;
572
    }
573
}
574