GenerateMarkdownDoc::documentPropertySignature()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 3
nc 3
nop 1
1
<?php
2
3
namespace Robo\Task\Development;
4
5
use Robo\Task\BaseTask;
6
use Robo\Result;
7
use Robo\Contract\BuilderAwareInterface;
8
use Robo\Common\BuilderAwareTrait;
9
10
/**
11
 * Simple documentation generator from source files.
12
 * Takes classes, properties and methods with their docblocks and writes down a markdown file.
13
 *
14
 * ``` php
15
 * <?php
16
 * $this->taskGenDoc('models.md')
17
 *      ->docClass('Model\User') // take class Model\User
18
 *      ->docClass('Model\Post') // take class Model\Post
19
 *      ->filterMethods(function(\ReflectionMethod $r) {
20
 *          return $r->isPublic() or $r->isProtected(); // process public and protected methods
21
 *      })->processClass(function(\ReflectionClass $r, $text) {
22
 *          return "Class ".$r->getName()."\n\n$text\n\n###Methods\n";
23
 *      })->run();
24
 * ```
25
 *
26
 * By default this task generates a documentation for each public method of a class, interface or trait.
27
 * It combines method signature with a docblock. Both can be post-processed.
28
 *
29
 * ``` php
30
 * <?php
31
 * $this->taskGenDoc('models.md')
32
 *      ->docClass('Model\User')
33
 *      ->processClassSignature(false) // false can be passed to not include class signature
34
 *      ->processClassDocBlock(function(\ReflectionClass $r, $text) {
35
 *          return "[This is part of application model]\n" . $text;
36
 *      })->processMethodSignature(function(\ReflectionMethod $r, $text) {
37
 *          return "#### {$r->name}()";
38
 *      })->processMethodDocBlock(function(\ReflectionMethod $r, $text) {
39
 *          return strpos($r->name, 'save')===0 ? "[Saves to the database]\n" . $text : $text;
40
 *      })->run();
41
 * ```
42
 */
43
class GenerateMarkdownDoc extends BaseTask implements BuilderAwareInterface
44
{
45
    use BuilderAwareTrait;
46
47
    /**
48
     * @var string[]
49
     */
50
    protected $docClass = [];
51
52
    /**
53
     * @var callable
54
     */
55
    protected $filterMethods;
56
57
    /**
58
     * @var callable
59
     */
60
    protected $filterClasses;
61
62
    /**
63
     * @var callable
64
     */
65
    protected $filterProperties;
66
67
    /**
68
     * @var callable
69
     */
70
    protected $processClass;
71
72
    /**
73
     * @var callable|false
74
     */
75
    protected $processClassSignature;
76
77
    /**
78
     * @var callable|false
79
     */
80
    protected $processClassDocBlock;
81
82
    /**
83
     * @var callable|false
84
     */
85
    protected $processMethod;
86
87
    /**
88
     * @var callable|false
89
     */
90
    protected $processMethodSignature;
91
92
    /**
93
     * @var callable|false
94
     */
95
    protected $processMethodDocBlock;
96
97
    /**
98
     * @var callable|false
99
     */
100
    protected $processProperty;
101
102
    /**
103
     * @var callable|false
104
     */
105
    protected $processPropertySignature;
106
107
    /**
108
     * @var callable|false
109
     */
110
    protected $processPropertyDocBlock;
111
112
    /**
113
     * @var callable
114
     */
115
    protected $reorder;
116
117
    /**
118
     * @var callable
119
     */
120
    protected $reorderMethods;
121
122
    /**
123
     * @todo Unused property.
124
     *
125
     * @var callable
126
     */
127
    protected $reorderProperties;
128
129
    /**
130
     * @var string
131
     */
132
    protected $filename;
133
134
    /**
135
     * @var string
136
     */
137
    protected $prepend = "";
138
139
    /**
140
     * @var string
141
     */
142
    protected $append = "";
143
144
    /**
145
     * @var string
146
     */
147
    protected $text;
148
149
    /**
150
     * @var string[]
151
     */
152
    protected $textForClass = [];
153
154
    /**
155
     * @param string $filename
156
     *
157
     * @return static
158
     */
159
    public static function init($filename)
160
    {
161
        return new static($filename);
162
    }
163
164
    /**
165
     * @param string $filename
166
     */
167
    public function __construct($filename)
168
    {
169
        $this->filename = $filename;
170
    }
171
172
    /**
173
     * Put a class you want to be documented.
174
     *
175
     * @param string $item
176
     *
177
     * @return $this
178
     */
179
    public function docClass($item)
180
    {
181
        $this->docClass[] = $item;
182
        return $this;
183
    }
184
185
    /**
186
     * Using a callback function filter out methods that won't be documented.
187
     *
188
     * @param callable $filterMethods
189
     *
190
     * @return $this
191
     */
192
    public function filterMethods($filterMethods)
193
    {
194
        $this->filterMethods = $filterMethods;
195
        return $this;
196
    }
197
198
    /**
199
     * Using a callback function filter out classes that won't be documented.
200
     *
201
     * @param callable $filterClasses
202
     *
203
     * @return $this
204
     */
205
    public function filterClasses($filterClasses)
206
    {
207
        $this->filterClasses = $filterClasses;
208
        return $this;
209
    }
210
211
    /**
212
     * Using a callback function filter out properties that won't be documented.
213
     *
214
     * @param callable $filterProperties
215
     *
216
     * @return $this
217
     */
218
    public function filterProperties($filterProperties)
219
    {
220
        $this->filterProperties = $filterProperties;
221
        return $this;
222
    }
223
224
    /**
225
     * Post-process class documentation.
226
     *
227
     * @param callable $processClass
228
     *
229
     * @return $this
230
     */
231
    public function processClass($processClass)
232
    {
233
        $this->processClass = $processClass;
234
        return $this;
235
    }
236
237
    /**
238
     * Post-process class signature. Provide *false* to skip.
239
     *
240
     * @param callable|false $processClassSignature
241
     *
242
     * @return $this
243
     */
244
    public function processClassSignature($processClassSignature)
245
    {
246
        $this->processClassSignature = $processClassSignature;
247
        return $this;
248
    }
249
250
    /**
251
     * Post-process class docblock contents. Provide *false* to skip.
252
     *
253
     * @param callable|false $processClassDocBlock
254
     *
255
     * @return $this
256
     */
257
    public function processClassDocBlock($processClassDocBlock)
258
    {
259
        $this->processClassDocBlock = $processClassDocBlock;
260
        return $this;
261
    }
262
263
    /**
264
     * Post-process method documentation. Provide *false* to skip.
265
     *
266
     * @param callable|false $processMethod
267
     *
268
     * @return $this
269
     */
270
    public function processMethod($processMethod)
271
    {
272
        $this->processMethod = $processMethod;
273
        return $this;
274
    }
275
276
    /**
277
     * Post-process method signature. Provide *false* to skip.
278
     *
279
     * @param callable|false $processMethodSignature
280
     *
281
     * @return $this
282
     */
283
    public function processMethodSignature($processMethodSignature)
284
    {
285
        $this->processMethodSignature = $processMethodSignature;
286
        return $this;
287
    }
288
289
    /**
290
     * Post-process method docblock contents. Provide *false* to skip.
291
     *
292
     * @param callable|false $processMethodDocBlock
293
     *
294
     * @return $this
295
     */
296
    public function processMethodDocBlock($processMethodDocBlock)
297
    {
298
        $this->processMethodDocBlock = $processMethodDocBlock;
299
        return $this;
300
    }
301
302
    /**
303
     * Post-process property documentation. Provide *false* to skip.
304
     *
305
     * @param callable|false $processProperty
306
     *
307
     * @return $this
308
     */
309
    public function processProperty($processProperty)
310
    {
311
        $this->processProperty = $processProperty;
312
        return $this;
313
    }
314
315
    /**
316
     * Post-process property signature. Provide *false* to skip.
317
     *
318
     * @param callable|false $processPropertySignature
319
     *
320
     * @return $this
321
     */
322
    public function processPropertySignature($processPropertySignature)
323
    {
324
        $this->processPropertySignature = $processPropertySignature;
325
        return $this;
326
    }
327
328
    /**
329
     * Post-process property docblock contents. Provide *false* to skip.
330
     *
331
     * @param callable|false $processPropertyDocBlock
332
     *
333
     * @return $this
334
     */
335
    public function processPropertyDocBlock($processPropertyDocBlock)
336
    {
337
        $this->processPropertyDocBlock = $processPropertyDocBlock;
338
        return $this;
339
    }
340
341
    /**
342
     * Use a function to reorder classes.
343
     *
344
     * @param callable $reorder
345
     *
346
     * @return $this
347
     */
348
    public function reorder($reorder)
349
    {
350
        $this->reorder = $reorder;
351
        return $this;
352
    }
353
354
    /**
355
     * Use a function to reorder methods in class.
356
     *
357
     * @param callable $reorderMethods
358
     *
359
     * @return $this
360
     */
361
    public function reorderMethods($reorderMethods)
362
    {
363
        $this->reorderMethods = $reorderMethods;
364
        return $this;
365
    }
366
367
    /**
368
     * @param callable $reorderProperties
369
     *
370
     * @return $this
371
     */
372
    public function reorderProperties($reorderProperties)
373
    {
374
        $this->reorderProperties = $reorderProperties;
375
        return $this;
376
    }
377
378
    /**
379
     * @param string $filename
380
     *
381
     * @return $this
382
     */
383
    public function filename($filename)
384
    {
385
        $this->filename = $filename;
386
        return $this;
387
    }
388
389
    /**
390
     * Inserts text at the beginning of markdown file.
391
     *
392
     * @param string $prepend
393
     *
394
     * @return $this
395
     */
396
    public function prepend($prepend)
397
    {
398
        $this->prepend = $prepend;
399
        return $this;
400
    }
401
402
    /**
403
     * Inserts text at the end of markdown file.
404
     *
405
     * @param string $append
406
     *
407
     * @return $this
408
     */
409
    public function append($append)
410
    {
411
        $this->append = $append;
412
        return $this;
413
    }
414
415
    /**
416
     * @param string $text
417
     *
418
     * @return $this
419
     */
420
    public function text($text)
421
    {
422
        $this->text = $text;
423
        return $this;
424
    }
425
426
    /**
427
     * @param string $item
428
     *
429
     * @return $this
430
     */
431
    public function textForClass($item)
432
    {
433
        $this->textForClass[] = $item;
434
        return $this;
435
    }
436
437
    /**
438
     * {@inheritdoc}
439
     */
440
    public function run()
441
    {
442
        foreach ($this->docClass as $class) {
443
            $this->printTaskInfo("Processing {class}", ['class' => $class]);
444
            $this->textForClass[$class] = $this->documentClass($class);
445
        }
446
447
        if (is_callable($this->reorder)) {
448
            $this->printTaskInfo("Applying reorder function");
449
            call_user_func_array($this->reorder, [$this->textForClass]);
450
        }
451
452
        $this->text = implode("\n", $this->textForClass);
453
454
        /** @var \Robo\Result $result */
455
        $result = $this->collectionBuilder()->taskWriteToFile($this->filename)
0 ignored issues
show
Documentation Bug introduced by
The method taskWriteToFile does not exist on object<Robo\Collection\CollectionBuilder>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
456
            ->line($this->prepend)
457
            ->text($this->text)
458
            ->line($this->append)
459
            ->run();
460
461
        $this->printTaskSuccess('{filename} created. {class-count} classes documented', ['filename' => $this->filename, 'class-count' => count($this->docClass)]);
462
463
        return new Result($this, $result->getExitCode(), $result->getMessage(), $this->textForClass);
464
    }
465
466
    /**
467
     * @param string $class
468
     *
469
     * @return null|string
470
     */
471
    protected function documentClass($class)
472
    {
473
        if (!class_exists($class) && !trait_exists($class)) {
474
            return "";
475
        }
476
        $refl = new \ReflectionClass($class);
477
478
        if (is_callable($this->filterClasses)) {
479
            $ret = call_user_func($this->filterClasses, $refl);
480
            if (!$ret) {
481
                return;
482
            }
483
        }
484
        $doc = $this->documentClassSignature($refl);
485
        $doc .= "\n" . $this->documentClassDocBlock($refl);
486
        $doc .= "\n";
487
488
        if (is_callable($this->processClass)) {
489
            $doc = call_user_func($this->processClass, $refl, $doc);
490
        }
491
492
        $properties = [];
493
        foreach ($refl->getProperties() as $reflProperty) {
494
            $properties[] = $this->documentProperty($reflProperty);
495
        }
496
497
        $properties = array_filter($properties);
498
        $doc .= implode("\n", $properties);
499
500
        $methods = [];
501
        foreach ($refl->getMethods() as $reflMethod) {
502
            $methods[$reflMethod->name] = $this->documentMethod($reflMethod);
503
        }
504
        if (is_callable($this->reorderMethods)) {
505
            call_user_func_array($this->reorderMethods, [&$methods]);
506
        }
507
508
        $methods = array_filter($methods);
509
510
        $doc .= implode("\n", $methods) . "\n";
511
512
        return $doc;
513
    }
514
515
    /**
516
     * @param \ReflectionClass $reflectionClass
517
     *
518
     * @return string
519
     */
520
    protected function documentClassSignature(\ReflectionClass $reflectionClass)
521
    {
522
        if ($this->processClassSignature === false) {
523
            return "";
524
        }
525
526
        $signature = "## {$reflectionClass->name}\n\n";
527
528
        if ($parent = $reflectionClass->getParentClass()) {
529
            $signature .= "* *Extends* `{$parent->name}`";
530
        }
531
        $interfaces = $reflectionClass->getInterfaceNames();
532
        if (count($interfaces)) {
533
            $signature .= "\n* *Implements* `" . implode('`, `', $interfaces) . '`';
534
        }
535
        $traits = $reflectionClass->getTraitNames();
536
        if (count($traits)) {
537
            $signature .= "\n* *Uses* `" . implode('`, `', $traits) . '`';
538
        }
539
        if (is_callable($this->processClassSignature)) {
540
            $signature = call_user_func($this->processClassSignature, $reflectionClass, $signature);
541
        }
542
543
        return $signature;
544
    }
545
546
    /**
547
     * @param \ReflectionClass $reflectionClass
548
     *
549
     * @return string
550
     */
551
    protected function documentClassDocBlock(\ReflectionClass $reflectionClass)
552
    {
553
        if ($this->processClassDocBlock === false) {
554
            return "";
555
        }
556
        $doc = self::indentDoc($reflectionClass->getDocComment());
557
        if (is_callable($this->processClassDocBlock)) {
558
            $doc = call_user_func($this->processClassDocBlock, $reflectionClass, $doc);
559
        }
560
        return $doc;
561
    }
562
563
    /**
564
     * @param \ReflectionMethod $reflectedMethod
565
     *
566
     * @return string
567
     */
568
    protected function documentMethod(\ReflectionMethod $reflectedMethod)
569
    {
570
        if ($this->processMethod === false) {
571
            return "";
572
        }
573 View Code Duplication
        if (is_callable($this->filterMethods)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
574
            $ret = call_user_func($this->filterMethods, $reflectedMethod);
575
            if (!$ret) {
576
                return "";
577
            }
578
        } else {
579
            if (!$reflectedMethod->isPublic()) {
580
                return "";
581
            }
582
        }
583
584
        $signature = $this->documentMethodSignature($reflectedMethod);
585
        $docblock = $this->documentMethodDocBlock($reflectedMethod);
586
        $methodDoc = "$signature $docblock";
587
        if (is_callable($this->processMethod)) {
588
            $methodDoc = call_user_func($this->processMethod, $reflectedMethod, $methodDoc);
589
        }
590
        return $methodDoc;
591
    }
592
593
    /**
594
     * @param \ReflectionProperty $reflectedProperty
595
     *
596
     * @return string
597
     */
598
    protected function documentProperty(\ReflectionProperty $reflectedProperty)
599
    {
600
        if ($this->processProperty === false) {
601
            return "";
602
        }
603 View Code Duplication
        if (is_callable($this->filterProperties)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
604
            $ret = call_user_func($this->filterProperties, $reflectedProperty);
605
            if (!$ret) {
606
                return "";
607
            }
608
        } else {
609
            if (!$reflectedProperty->isPublic()) {
610
                return "";
611
            }
612
        }
613
        $signature = $this->documentPropertySignature($reflectedProperty);
614
        $docblock = $this->documentPropertyDocBlock($reflectedProperty);
615
        $propertyDoc = $signature . $docblock;
616
        if (is_callable($this->processProperty)) {
617
            $propertyDoc = call_user_func($this->processProperty, $reflectedProperty, $propertyDoc);
618
        }
619
        return $propertyDoc;
620
    }
621
622
    /**
623
     * @param \ReflectionProperty $reflectedProperty
624
     *
625
     * @return string
626
     */
627
    protected function documentPropertySignature(\ReflectionProperty $reflectedProperty)
628
    {
629
        if ($this->processPropertySignature === false) {
630
            return "";
631
        }
632
        $modifiers = implode(' ', \Reflection::getModifierNames($reflectedProperty->getModifiers()));
633
        $signature = "#### *$modifiers* {$reflectedProperty->name}";
634
        if (is_callable($this->processPropertySignature)) {
635
            $signature = call_user_func($this->processPropertySignature, $reflectedProperty, $signature);
636
        }
637
        return $signature;
638
    }
639
640
    /**
641
     * @param \ReflectionProperty $reflectedProperty
642
     *
643
     * @return string
644
     */
645
    protected function documentPropertyDocBlock(\ReflectionProperty $reflectedProperty)
646
    {
647
        if ($this->processPropertyDocBlock === false) {
648
            return "";
649
        }
650
        $propertyDoc = $reflectedProperty->getDocComment();
651
        // take from parent
652
        if (!$propertyDoc) {
653
            $parent = $reflectedProperty->getDeclaringClass();
654
            while ($parent = $parent->getParentClass()) {
655
                if ($parent->hasProperty($reflectedProperty->name)) {
656
                    $propertyDoc = $parent->getProperty($reflectedProperty->name)->getDocComment();
657
                }
658
            }
659
        }
660
        $propertyDoc = self::indentDoc($propertyDoc, 7);
661
        $propertyDoc = preg_replace("~^@(.*?)([$\s])~", ' * `$1` $2', $propertyDoc); // format annotations
662
        if (is_callable($this->processPropertyDocBlock)) {
663
            $propertyDoc = call_user_func($this->processPropertyDocBlock, $reflectedProperty, $propertyDoc);
664
        }
665
        return ltrim($propertyDoc);
666
    }
667
668
    /**
669
     * @param \ReflectionParameter $param
670
     *
671
     * @return string
672
     */
673
    protected function documentParam(\ReflectionParameter $param)
674
    {
675
        $text = "";
676
        if ($param->isArray()) {
677
            $text .= 'array ';
678
        }
679
        if ($param->isCallable()) {
680
            $text .= 'callable ';
681
        }
682
        $text .= '$' . $param->name;
683
        if ($param->isDefaultValueAvailable()) {
684
            if ($param->allowsNull()) {
685
                $text .= ' = null';
686
            } else {
687
                $text .= ' = ' . str_replace("\n", ' ', print_r($param->getDefaultValue(), true));
688
            }
689
        }
690
691
        return $text;
692
    }
693
694
    /**
695
     * @param string $doc
696
     * @param int $indent
697
     *
698
     * @return string
699
     */
700
    public static function indentDoc($doc, $indent = 3)
701
    {
702
        if (!$doc) {
703
            return $doc;
704
        }
705
        return implode(
706
            "\n",
707
            array_map(
708
                function ($line) use ($indent) {
709
                    return substr($line, $indent);
710
                },
711
                explode("\n", $doc)
712
            )
713
        );
714
    }
715
716
    /**
717
     * @param \ReflectionMethod $reflectedMethod
718
     *
719
     * @return string
720
     */
721
    protected function documentMethodSignature(\ReflectionMethod $reflectedMethod)
722
    {
723
        if ($this->processMethodSignature === false) {
724
            return "";
725
        }
726
        $modifiers = implode(' ', \Reflection::getModifierNames($reflectedMethod->getModifiers()));
727
        $params = implode(
728
            ', ',
729
            array_map(
730
                function ($p) {
731
                    return $this->documentParam($p);
732
                },
733
                $reflectedMethod->getParameters()
734
            )
735
        );
736
        $signature = "#### *$modifiers* {$reflectedMethod->name}($params)";
737
        if (is_callable($this->processMethodSignature)) {
738
            $signature = call_user_func($this->processMethodSignature, $reflectedMethod, $signature);
739
        }
740
        return $signature;
741
    }
742
743
    /**
744
     * @param \ReflectionMethod $reflectedMethod
745
     *
746
     * @return string
747
     */
748
    protected function documentMethodDocBlock(\ReflectionMethod $reflectedMethod)
749
    {
750
        if ($this->processMethodDocBlock === false) {
751
            return "";
752
        }
753
        $methodDoc = $reflectedMethod->getDocComment();
754
        // take from parent
755
        if (!$methodDoc) {
756
            $parent = $reflectedMethod->getDeclaringClass();
757
            while ($parent = $parent->getParentClass()) {
758
                if ($parent->hasMethod($reflectedMethod->name)) {
759
                    $methodDoc = $parent->getMethod($reflectedMethod->name)->getDocComment();
760
                }
761
            }
762
        }
763
        // take from interface
764
        if (!$methodDoc) {
765
            $interfaces = $reflectedMethod->getDeclaringClass()->getInterfaces();
766
            foreach ($interfaces as $interface) {
767
                $i = new \ReflectionClass($interface->name);
768
                if ($i->hasMethod($reflectedMethod->name)) {
769
                    $methodDoc = $i->getMethod($reflectedMethod->name)->getDocComment();
770
                    break;
771
                }
772
            }
773
        }
774
775
        $methodDoc = self::indentDoc($methodDoc, 7);
776
        $methodDoc = preg_replace("~^@(.*?) ([$\s])~m", ' * `$1` $2', $methodDoc); // format annotations
777
        if (is_callable($this->processMethodDocBlock)) {
778
            $methodDoc = call_user_func($this->processMethodDocBlock, $reflectedMethod, $methodDoc);
779
        }
780
781
        return $methodDoc;
782
    }
783
}
784