Completed
Push — master ( 8b8afe...5f2bbe )
by Greg
02:21
created

src/Task/Development/GenerateMarkdownDoc.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
namespace Robo\Task\Development;
3
4
use Robo\Task\BaseTask;
5
use Robo\Result;
6
use Robo\Contract\BuilderAwareInterface;
7
use Robo\Common\BuilderAwareTrait;
8
9
/**
10
 * Simple documentation generator from source files.
11
 * Takes classes, properties and methods with their docblocks and writes down a markdown file.
12
 *
13
 * ``` php
14
 * <?php
15
 * $this->taskGenDoc('models.md')
16
 *      ->docClass('Model\User') // take class Model\User
17
 *      ->docClass('Model\Post') // take class Model\Post
18
 *      ->filterMethods(function(\ReflectionMethod $r) {
19
 *          return $r->isPublic() or $r->isProtected(); // process public and protected methods
20
 *      })->processClass(function(\ReflectionClass $r, $text) {
21
 *          return "Class ".$r->getName()."\n\n$text\n\n###Methods\n";
22
 *      })->run();
23
 * ```
24
 *
25
 * By default this task generates a documentation for each public method of a class, interface or trait.
26
 * It combines method signature with a docblock. Both can be post-processed.
27
 *
28
 * ``` php
29
 * <?php
30
 * $this->taskGenDoc('models.md')
31
 *      ->docClass('Model\User')
32
 *      ->processClassSignature(false) // false can be passed to not include class signature
33
 *      ->processClassDocBlock(function(\ReflectionClass $r, $text) {
34
 *          return "[This is part of application model]\n" . $text;
35
 *      })->processMethodSignature(function(\ReflectionMethod $r, $text) {
36
 *          return "#### {$r->name}()";
37
 *      })->processMethodDocBlock(function(\ReflectionMethod $r, $text) {
38
 *          return strpos($r->name, 'save')===0 ? "[Saves to the database]\n" . $text : $text;
39
 *      })->run();
40
 * ```
41
 */
42
class GenerateMarkdownDoc extends BaseTask implements BuilderAwareInterface
43
{
44
    use BuilderAwareTrait;
45
46
    /**
47
     * @var string[]
48
     */
49
    protected $docClass = [];
50
51
    /**
52
     * @var callable
53
     */
54
    protected $filterMethods;
55
56
    /**
57
     * @var callable
58
     */
59
    protected $filterClasses;
60
61
    /**
62
     * @var callable
63
     */
64
    protected $filterProperties;
65
66
    /**
67
     * @var callable
68
     */
69
    protected $processClass;
70
71
    /**
72
     * @var callable|false
73
     */
74
    protected $processClassSignature;
75
76
    /**
77
     * @var callable|false
78
     */
79
    protected $processClassDocBlock;
80
81
    /**
82
     * @var callable|false
83
     */
84
    protected $processMethod;
85
86
    /**
87
     * @var callable|false
88
     */
89
    protected $processMethodSignature;
90
91
    /**
92
     * @var callable|false
93
     */
94
    protected $processMethodDocBlock;
95
96
    /**
97
     * @var callable|false
98
     */
99
    protected $processProperty;
100
101
    /**
102
     * @var callable|false
103
     */
104
    protected $processPropertySignature;
105
106
    /**
107
     * @var callable|false
108
     */
109
    protected $processPropertyDocBlock;
110
111
    /**
112
     * @var callable
113
     */
114
    protected $reorder;
115
116
    /**
117
     * @var callable
118
     */
119
    protected $reorderMethods;
120
121
    /**
122
     * @todo Unused property.
123
     *
124
     * @var callable
125
     */
126
    protected $reorderProperties;
127
128
    /**
129
     * @var string
130
     */
131
    protected $filename;
132
133
    /**
134
     * @var string
135
     */
136
    protected $prepend = "";
137
138
    /**
139
     * @var string
140
     */
141
    protected $append = "";
142
143
    /**
144
     * @var string
145
     */
146
    protected $text;
147
148
    /**
149
     * @var string[]
150
     */
151
    protected $textForClass = [];
152
153
    /**
154
     * @param string $filename
155
     *
156
     * @return static
157
     */
158
    public static function init($filename)
159
    {
160
        return new static($filename);
161
    }
162
163
    /**
164
     * @param string $filename
165
     */
166
    public function __construct($filename)
167
    {
168
        $this->filename = $filename;
169
    }
170
171
    /**
172
     * Put a class you want to be documented.
173
     *
174
     * @param string $item
175
     *
176
     * @return $this
177
     */
178
    public function docClass($item)
179
    {
180
        $this->docClass[] = $item;
181
        return $this;
182
    }
183
184
    /**
185
     * Using a callback function filter out methods that won't be documented.
186
     *
187
     * @param callable $filterMethods
188
     *
189
     * @return $this
190
     */
191
    public function filterMethods($filterMethods)
192
    {
193
        $this->filterMethods = $filterMethods;
194
        return $this;
195
    }
196
197
    /**
198
     * Using a callback function filter out classes that won't be documented.
199
     *
200
     * @param callable $filterClasses
201
     *
202
     * @return $this
203
     */
204
    public function filterClasses($filterClasses)
205
    {
206
        $this->filterClasses = $filterClasses;
207
        return $this;
208
    }
209
210
    /**
211
     * Using a callback function filter out properties that won't be documented.
212
     *
213
     * @param callable $filterProperties
214
     *
215
     * @return $this
216
     */
217
    public function filterProperties($filterProperties)
218
    {
219
        $this->filterProperties = $filterProperties;
220
        return $this;
221
    }
222
223
    /**
224
     * Post-process class documentation.
225
     *
226
     * @param callable $processClass
227
     *
228
     * @return $this
229
     */
230
    public function processClass($processClass)
231
    {
232
        $this->processClass = $processClass;
233
        return $this;
234
    }
235
236
    /**
237
     * Post-process class signature. Provide *false* to skip.
238
     *
239
     * @param callable|false $processClassSignature
240
     *
241
     * @return $this
242
     */
243
    public function processClassSignature($processClassSignature)
244
    {
245
        $this->processClassSignature = $processClassSignature;
246
        return $this;
247
    }
248
249
    /**
250
     * Post-process class docblock contents. Provide *false* to skip.
251
     *
252
     * @param callable|false $processClassDocBlock
253
     *
254
     * @return $this
255
     */
256
    public function processClassDocBlock($processClassDocBlock)
257
    {
258
        $this->processClassDocBlock = $processClassDocBlock;
259
        return $this;
260
    }
261
262
    /**
263
     * Post-process method documentation. Provide *false* to skip.
264
     *
265
     * @param callable|false $processMethod
266
     *
267
     * @return $this
268
     */
269
    public function processMethod($processMethod)
270
    {
271
        $this->processMethod = $processMethod;
272
        return $this;
273
    }
274
275
    /**
276
     * Post-process method signature. Provide *false* to skip.
277
     *
278
     * @param callable|false $processMethodSignature
279
     *
280
     * @return $this
281
     */
282
    public function processMethodSignature($processMethodSignature)
283
    {
284
        $this->processMethodSignature = $processMethodSignature;
285
        return $this;
286
    }
287
288
    /**
289
     * Post-process method docblock contents. Provide *false* to skip.
290
     *
291
     * @param callable|false $processMethodDocBlock
292
     *
293
     * @return $this
294
     */
295
    public function processMethodDocBlock($processMethodDocBlock)
296
    {
297
        $this->processMethodDocBlock = $processMethodDocBlock;
298
        return $this;
299
    }
300
301
    /**
302
     * Post-process property documentation. Provide *false* to skip.
303
     *
304
     * @param callable|false $processProperty
305
     *
306
     * @return $this
307
     */
308
    public function processProperty($processProperty)
309
    {
310
        $this->processProperty = $processProperty;
311
        return $this;
312
    }
313
314
    /**
315
     * Post-process property signature. Provide *false* to skip.
316
     *
317
     * @param callable|false $processPropertySignature
318
     *
319
     * @return $this
320
     */
321
    public function processPropertySignature($processPropertySignature)
322
    {
323
        $this->processPropertySignature = $processPropertySignature;
324
        return $this;
325
    }
326
327
    /**
328
     * Post-process property docblock contents. Provide *false* to skip.
329
     *
330
     * @param callable|false $processPropertyDocBlock
331
     *
332
     * @return $this
333
     */
334
    public function processPropertyDocBlock($processPropertyDocBlock)
335
    {
336
        $this->processPropertyDocBlock = $processPropertyDocBlock;
337
        return $this;
338
    }
339
340
    /**
341
     * Use a function to reorder classes.
342
     *
343
     * @param callable $reorder
344
     *
345
     * @return $this
346
     */
347
    public function reorder($reorder)
348
    {
349
        $this->reorder = $reorder;
350
        return $this;
351
    }
352
353
    /**
354
     * Use a function to reorder methods in class.
355
     *
356
     * @param callable $reorderMethods
357
     *
358
     * @return $this
359
     */
360
    public function reorderMethods($reorderMethods)
361
    {
362
        $this->reorderMethods = $reorderMethods;
363
        return $this;
364
    }
365
366
    /**
367
     * @param callable $reorderProperties
368
     *
369
     * @return $this
370
     */
371
    public function reorderProperties($reorderProperties)
372
    {
373
        $this->reorderProperties = $reorderProperties;
374
        return $this;
375
    }
376
377
    /**
378
     * @param string $filename
379
     *
380
     * @return $this
381
     */
382
    public function filename($filename)
383
    {
384
        $this->filename = $filename;
385
        return $this;
386
    }
387
388
    /**
389
     * Inserts text at the beginning of markdown file.
390
     *
391
     * @param string $prepend
392
     *
393
     * @return $this
394
     */
395
    public function prepend($prepend)
396
    {
397
        $this->prepend = $prepend;
398
        return $this;
399
    }
400
401
    /**
402
     * Inserts text at the end of markdown file.
403
     *
404
     * @param string $append
405
     *
406
     * @return $this
407
     */
408
    public function append($append)
409
    {
410
        $this->append = $append;
411
        return $this;
412
    }
413
414
    /**
415
     * @param string $text
416
     *
417
     * @return $this
418
     */
419
    public function text($text)
420
    {
421
        $this->text = $text;
422
        return $this;
423
    }
424
425
    /**
426
     * @param string $item
427
     *
428
     * @return $this
429
     */
430
    public function textForClass($item)
431
    {
432
        $this->textForClass[] = $item;
433
        return $this;
434
    }
435
436
    /**
437
     * {@inheritdoc}
438
     */
439
    public function run()
440
    {
441
        foreach ($this->docClass as $class) {
442
            $this->printTaskInfo("Processing {class}", ['class' => $class]);
443
            $this->textForClass[$class] = $this->documentClass($class);
444
        }
445
446
        if (is_callable($this->reorder)) {
447
            $this->printTaskInfo("Applying reorder function");
448
            call_user_func_array($this->reorder, [$this->textForClass]);
449
        }
450
451
        $this->text = implode("\n", $this->textForClass);
452
453
        /** @var \Robo\Result $result */
454
        $result = $this->collectionBuilder()->taskWriteToFile($this->filename)
455
            ->line($this->prepend)
456
            ->text($this->text)
457
            ->line($this->append)
458
            ->run();
459
460
        $this->printTaskSuccess('{filename} created. {class-count} classes documented', ['filename' => $this->filename, 'class-count' => count($this->docClass)]);
461
462
        return new Result($this, $result->getExitCode(), $result->getMessage(), $this->textForClass);
463
    }
464
465
    /**
466
     * @param string $class
467
     *
468
     * @return null|string
469
     */
470
    protected function documentClass($class)
471
    {
472
        if (!class_exists($class) && !trait_exists($class)) {
473
            return "";
474
        }
475
        $refl = new \ReflectionClass($class);
476
477
        if (is_callable($this->filterClasses)) {
478
            $ret = call_user_func($this->filterClasses, $refl);
479
            if (!$ret) {
480
                return;
481
            }
482
        }
483
        $doc = $this->documentClassSignature($refl);
484
        $doc .= "\n" . $this->documentClassDocBlock($refl);
485
        $doc .= "\n";
486
487
        if (is_callable($this->processClass)) {
488
            $doc = call_user_func($this->processClass, $refl, $doc);
489
        }
490
491
        $properties = [];
492
        foreach ($refl->getProperties() as $reflProperty) {
493
            $properties[] = $this->documentProperty($reflProperty);
494
        }
495
496
        $properties = array_filter($properties);
497
        $doc .= implode("\n", $properties);
498
499
        $methods = [];
500
        foreach ($refl->getMethods() as $reflMethod) {
501
            $methods[$reflMethod->name] = $this->documentMethod($reflMethod);
502
        }
503
        if (is_callable($this->reorderMethods)) {
504
            call_user_func_array($this->reorderMethods, [&$methods]);
505
        }
506
507
        $methods = array_filter($methods);
508
509
        $doc .= implode("\n", $methods)."\n";
510
511
        return $doc;
512
    }
513
514
    /**
515
     * @param \ReflectionClass $reflectionClass
516
     *
517
     * @return string
518
     */
519
    protected function documentClassSignature(\ReflectionClass $reflectionClass)
520
    {
521
        if ($this->processClassSignature === false) {
522
            return "";
523
        }
524
525
        $signature = "## {$reflectionClass->name}\n\n";
526
527
        if ($parent = $reflectionClass->getParentClass()) {
528
            $signature .= "* *Extends* `{$parent->name}`";
529
        }
530
        $interfaces = $reflectionClass->getInterfaceNames();
531
        if (count($interfaces)) {
532
            $signature .= "\n* *Implements* `" . implode('`, `', $interfaces) . '`';
533
        }
534
        $traits = $reflectionClass->getTraitNames();
535
        if (count($traits)) {
536
            $signature .= "\n* *Uses* `" . implode('`, `', $traits) . '`';
537
        }
538
        if (is_callable($this->processClassSignature)) {
539
            $signature = call_user_func($this->processClassSignature, $reflectionClass, $signature);
540
        }
541
542
        return $signature;
543
    }
544
545
    /**
546
     * @param \ReflectionClass $reflectionClass
547
     *
548
     * @return string
549
     */
550
    protected function documentClassDocBlock(\ReflectionClass $reflectionClass)
551
    {
552
        if ($this->processClassDocBlock === false) {
553
            return "";
554
        }
555
        $doc = self::indentDoc($reflectionClass->getDocComment());
556
        if (is_callable($this->processClassDocBlock)) {
557
            $doc = call_user_func($this->processClassDocBlock, $reflectionClass, $doc);
558
        }
559
        return $doc;
560
    }
561
562
    /**
563
     * @param \ReflectionMethod $reflectedMethod
564
     *
565
     * @return string
566
     */
567
    protected function documentMethod(\ReflectionMethod $reflectedMethod)
568
    {
569
        if ($this->processMethod === false) {
570
            return "";
571
        }
572 View Code Duplication
        if (is_callable($this->filterMethods)) {
0 ignored issues
show
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...
573
            $ret = call_user_func($this->filterMethods, $reflectedMethod);
574
            if (!$ret) {
575
                return "";
576
            }
577
        } else {
578
            if (!$reflectedMethod->isPublic()) {
579
                return "";
580
            }
581
        }
582
583
        $signature = $this->documentMethodSignature($reflectedMethod);
584
        $docblock = $this->documentMethodDocBlock($reflectedMethod);
585
        $methodDoc = "$signature $docblock";
586
        if (is_callable($this->processMethod)) {
587
            $methodDoc = call_user_func($this->processMethod, $reflectedMethod, $methodDoc);
588
        }
589
        return $methodDoc;
590
    }
591
592
    /**
593
     * @param \ReflectionProperty $reflectedProperty
594
     *
595
     * @return string
596
     */
597
    protected function documentProperty(\ReflectionProperty $reflectedProperty)
598
    {
599
        if ($this->processProperty === false) {
600
            return "";
601
        }
602 View Code Duplication
        if (is_callable($this->filterProperties)) {
0 ignored issues
show
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...
603
            $ret = call_user_func($this->filterProperties, $reflectedProperty);
604
            if (!$ret) {
605
                return "";
606
            }
607
        } else {
608
            if (!$reflectedProperty->isPublic()) {
609
                return "";
610
            }
611
        }
612
        $signature = $this->documentPropertySignature($reflectedProperty);
613
        $docblock = $this->documentPropertyDocBlock($reflectedProperty);
614
        $propertyDoc = $signature . $docblock;
615
        if (is_callable($this->processProperty)) {
616
            $propertyDoc = call_user_func($this->processProperty, $reflectedProperty, $propertyDoc);
617
        }
618
        return $propertyDoc;
619
    }
620
621
    /**
622
     * @param \ReflectionProperty $reflectedProperty
623
     *
624
     * @return string
625
     */
626
    protected function documentPropertySignature(\ReflectionProperty $reflectedProperty)
627
    {
628
        if ($this->processPropertySignature === false) {
629
            return "";
630
        }
631
        $modifiers = implode(' ', \Reflection::getModifierNames($reflectedProperty->getModifiers()));
632
        $signature = "#### *$modifiers* {$reflectedProperty->name}";
633
        if (is_callable($this->processPropertySignature)) {
634
            $signature = call_user_func($this->processPropertySignature, $reflectedProperty, $signature);
635
        }
636
        return $signature;
637
    }
638
639
    /**
640
     * @param \ReflectionProperty $reflectedProperty
641
     *
642
     * @return string
643
     */
644
    protected function documentPropertyDocBlock(\ReflectionProperty $reflectedProperty)
645
    {
646
        if ($this->processPropertyDocBlock === false) {
647
            return "";
648
        }
649
        $propertyDoc = $reflectedProperty->getDocComment();
650
        // take from parent
651
        if (!$propertyDoc) {
652
            $parent = $reflectedProperty->getDeclaringClass();
653
            while ($parent = $parent->getParentClass()) {
654
                if ($parent->hasProperty($reflectedProperty->name)) {
655
                    $propertyDoc = $parent->getProperty($reflectedProperty->name)->getDocComment();
656
                }
657
            }
658
        }
659
        $propertyDoc = self::indentDoc($propertyDoc, 7);
660
        $propertyDoc = preg_replace("~^@(.*?)([$\s])~", ' * `$1` $2', $propertyDoc); // format annotations
661
        if (is_callable($this->processPropertyDocBlock)) {
662
            $propertyDoc = call_user_func($this->processPropertyDocBlock, $reflectedProperty, $propertyDoc);
663
        }
664
        return ltrim($propertyDoc);
665
    }
666
667
    /**
668
     * @param \ReflectionParameter $param
669
     *
670
     * @return string
671
     */
672
    protected function documentParam(\ReflectionParameter $param)
673
    {
674
        $text = "";
675
        if ($param->isArray()) {
676
            $text .= 'array ';
677
        }
678
        if ($param->isCallable()) {
679
            $text .= 'callable ';
680
        }
681
        $text .= '$' . $param->name;
682
        if ($param->isDefaultValueAvailable()) {
683
            if ($param->allowsNull()) {
684
                $text .= ' = null';
685
            } else {
686
                $text .= ' = ' . str_replace("\n", ' ', print_r($param->getDefaultValue(), true));
687
            }
688
        }
689
690
        return $text;
691
    }
692
693
    /**
694
     * @param string $doc
695
     * @param int $indent
696
     *
697
     * @return string
698
     */
699
    public static function indentDoc($doc, $indent = 3)
700
    {
701
        if (!$doc) {
702
            return $doc;
703
        }
704
        return implode(
705
            "\n",
706
            array_map(
707
                function ($line) use ($indent) {
708
                    return substr($line, $indent);
709
                },
710
                explode("\n", $doc)
711
            )
712
        );
713
    }
714
715
    /**
716
     * @param \ReflectionMethod $reflectedMethod
717
     *
718
     * @return string
719
     */
720
    protected function documentMethodSignature(\ReflectionMethod $reflectedMethod)
721
    {
722
        if ($this->processMethodSignature === false) {
723
            return "";
724
        }
725
        $modifiers = implode(' ', \Reflection::getModifierNames($reflectedMethod->getModifiers()));
726
        $params = implode(
727
            ', ',
728
            array_map(
729
                function ($p) {
730
                    return $this->documentParam($p);
731
                },
732
                $reflectedMethod->getParameters()
733
            )
734
        );
735
        $signature = "#### *$modifiers* {$reflectedMethod->name}($params)";
736
        if (is_callable($this->processMethodSignature)) {
737
            $signature = call_user_func($this->processMethodSignature, $reflectedMethod, $signature);
738
        }
739
        return $signature;
740
    }
741
742
    /**
743
     * @param \ReflectionMethod $reflectedMethod
744
     *
745
     * @return string
746
     */
747
    protected function documentMethodDocBlock(\ReflectionMethod $reflectedMethod)
748
    {
749
        if ($this->processMethodDocBlock === false) {
750
            return "";
751
        }
752
        $methodDoc = $reflectedMethod->getDocComment();
753
        // take from parent
754
        if (!$methodDoc) {
755
            $parent = $reflectedMethod->getDeclaringClass();
756
            while ($parent = $parent->getParentClass()) {
757
                if ($parent->hasMethod($reflectedMethod->name)) {
758
                    $methodDoc = $parent->getMethod($reflectedMethod->name)->getDocComment();
759
                }
760
            }
761
        }
762
        // take from interface
763
        if (!$methodDoc) {
764
            $interfaces = $reflectedMethod->getDeclaringClass()->getInterfaces();
765
            foreach ($interfaces as $interface) {
766
                $i = new \ReflectionClass($interface->name);
767
                if ($i->hasMethod($reflectedMethod->name)) {
768
                    $methodDoc = $i->getMethod($reflectedMethod->name)->getDocComment();
769
                    break;
770
                }
771
            }
772
        }
773
774
        $methodDoc = self::indentDoc($methodDoc, 7);
775
        $methodDoc = preg_replace("~^@(.*?) ([$\s])~m", ' * `$1` $2', $methodDoc); // format annotations
776
        if (is_callable($this->processMethodDocBlock)) {
777
            $methodDoc = call_user_func($this->processMethodDocBlock, $reflectedMethod, $methodDoc);
778
        }
779
780
        return $methodDoc;
781
    }
782
}
783