Completed
Push — master ( 34e0af...5c2b62 )
by Greg
02:42
created

src/Task/Development/GenerateMarkdownDoc.php (1 issue)

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.
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)
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...
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)) {
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)) {
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)) {
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