Updated to Drupal 8.5. Core Media not yet in use.
[yaffs-website] / vendor / consolidation / robo / src / Task / Development / GenerateMarkdownDoc.php
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         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         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 }