2 namespace Robo\Task\Development;
4 use Robo\Task\BaseTask;
6 use Robo\Contract\BuilderAwareInterface;
7 use Robo\Common\BuilderAwareTrait;
10 * Simple documentation generator from source files.
11 * Takes classes, properties and methods with their docblocks and writes down a markdown file.
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";
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.
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;
42 class GenerateMarkdownDoc extends BaseTask implements BuilderAwareInterface
44 use BuilderAwareTrait;
49 protected $docClass = [];
54 protected $filterMethods;
59 protected $filterClasses;
64 protected $filterProperties;
69 protected $processClass;
74 protected $processClassSignature;
79 protected $processClassDocBlock;
84 protected $processMethod;
89 protected $processMethodSignature;
94 protected $processMethodDocBlock;
99 protected $processProperty;
102 * @var callable|false
104 protected $processPropertySignature;
107 * @var callable|false
109 protected $processPropertyDocBlock;
119 protected $reorderMethods;
122 * @todo Unused property.
126 protected $reorderProperties;
136 protected $prepend = "";
141 protected $append = "";
151 protected $textForClass = [];
154 * @param string $filename
158 public static function init($filename)
160 return new static($filename);
164 * @param string $filename
166 public function __construct($filename)
168 $this->filename = $filename;
172 * Put a class you want to be documented.
174 * @param string $item
178 public function docClass($item)
180 $this->docClass[] = $item;
185 * Using a callback function filter out methods that won't be documented.
187 * @param callable $filterMethods
191 public function filterMethods($filterMethods)
193 $this->filterMethods = $filterMethods;
198 * Using a callback function filter out classes that won't be documented.
200 * @param callable $filterClasses
204 public function filterClasses($filterClasses)
206 $this->filterClasses = $filterClasses;
211 * Using a callback function filter out properties that won't be documented.
213 * @param callable $filterProperties
217 public function filterProperties($filterProperties)
219 $this->filterProperties = $filterProperties;
224 * Post-process class documentation.
226 * @param callable $processClass
230 public function processClass($processClass)
232 $this->processClass = $processClass;
237 * Post-process class signature. Provide *false* to skip.
239 * @param callable|false $processClassSignature
243 public function processClassSignature($processClassSignature)
245 $this->processClassSignature = $processClassSignature;
250 * Post-process class docblock contents. Provide *false* to skip.
252 * @param callable|false $processClassDocBlock
256 public function processClassDocBlock($processClassDocBlock)
258 $this->processClassDocBlock = $processClassDocBlock;
263 * Post-process method documentation. Provide *false* to skip.
265 * @param callable|false $processMethod
269 public function processMethod($processMethod)
271 $this->processMethod = $processMethod;
276 * Post-process method signature. Provide *false* to skip.
278 * @param callable|false $processMethodSignature
282 public function processMethodSignature($processMethodSignature)
284 $this->processMethodSignature = $processMethodSignature;
289 * Post-process method docblock contents. Provide *false* to skip.
291 * @param callable|false $processMethodDocBlock
295 public function processMethodDocBlock($processMethodDocBlock)
297 $this->processMethodDocBlock = $processMethodDocBlock;
302 * Post-process property documentation. Provide *false* to skip.
304 * @param callable|false $processProperty
308 public function processProperty($processProperty)
310 $this->processProperty = $processProperty;
315 * Post-process property signature. Provide *false* to skip.
317 * @param callable|false $processPropertySignature
321 public function processPropertySignature($processPropertySignature)
323 $this->processPropertySignature = $processPropertySignature;
328 * Post-process property docblock contents. Provide *false* to skip.
330 * @param callable|false $processPropertyDocBlock
334 public function processPropertyDocBlock($processPropertyDocBlock)
336 $this->processPropertyDocBlock = $processPropertyDocBlock;
341 * Use a function to reorder classes.
343 * @param callable $reorder
347 public function reorder($reorder)
349 $this->reorder = $reorder;
354 * Use a function to reorder methods in class.
356 * @param callable $reorderMethods
360 public function reorderMethods($reorderMethods)
362 $this->reorderMethods = $reorderMethods;
367 * @param callable $reorderProperties
371 public function reorderProperties($reorderProperties)
373 $this->reorderProperties = $reorderProperties;
378 * @param string $filename
382 public function filename($filename)
384 $this->filename = $filename;
389 * Inserts text at the beginning of markdown file.
391 * @param string $prepend
395 public function prepend($prepend)
397 $this->prepend = $prepend;
402 * Inserts text at the end of markdown file.
404 * @param string $append
408 public function append($append)
410 $this->append = $append;
415 * @param string $text
419 public function text($text)
426 * @param string $item
430 public function textForClass($item)
432 $this->textForClass[] = $item;
439 public function run()
441 foreach ($this->docClass as $class) {
442 $this->printTaskInfo("Processing {class}", ['class' => $class]);
443 $this->textForClass[$class] = $this->documentClass($class);
446 if (is_callable($this->reorder)) {
447 $this->printTaskInfo("Applying reorder function");
448 call_user_func_array($this->reorder, [$this->textForClass]);
451 $this->text = implode("\n", $this->textForClass);
453 /** @var \Robo\Result $result */
454 $result = $this->collectionBuilder()->taskWriteToFile($this->filename)
455 ->line($this->prepend)
457 ->line($this->append)
460 $this->printTaskSuccess('{filename} created. {class-count} classes documented', ['filename' => $this->filename, 'class-count' => count($this->docClass)]);
462 return new Result($this, $result->getExitCode(), $result->getMessage(), $this->textForClass);
466 * @param string $class
468 * @return null|string
470 protected function documentClass($class)
472 if (!class_exists($class) && !trait_exists($class)) {
475 $refl = new \ReflectionClass($class);
477 if (is_callable($this->filterClasses)) {
478 $ret = call_user_func($this->filterClasses, $refl);
483 $doc = $this->documentClassSignature($refl);
484 $doc .= "\n" . $this->documentClassDocBlock($refl);
487 if (is_callable($this->processClass)) {
488 $doc = call_user_func($this->processClass, $refl, $doc);
492 foreach ($refl->getProperties() as $reflProperty) {
493 $properties[] = $this->documentProperty($reflProperty);
496 $properties = array_filter($properties);
497 $doc .= implode("\n", $properties);
500 foreach ($refl->getMethods() as $reflMethod) {
501 $methods[$reflMethod->name] = $this->documentMethod($reflMethod);
503 if (is_callable($this->reorderMethods)) {
504 call_user_func_array($this->reorderMethods, [&$methods]);
507 $methods = array_filter($methods);
509 $doc .= implode("\n", $methods)."\n";
515 * @param \ReflectionClass $reflectionClass
519 protected function documentClassSignature(\ReflectionClass $reflectionClass)
521 if ($this->processClassSignature === false) {
525 $signature = "## {$reflectionClass->name}\n\n";
527 if ($parent = $reflectionClass->getParentClass()) {
528 $signature .= "* *Extends* `{$parent->name}`";
530 $interfaces = $reflectionClass->getInterfaceNames();
531 if (count($interfaces)) {
532 $signature .= "\n* *Implements* `" . implode('`, `', $interfaces) . '`';
534 $traits = $reflectionClass->getTraitNames();
535 if (count($traits)) {
536 $signature .= "\n* *Uses* `" . implode('`, `', $traits) . '`';
538 if (is_callable($this->processClassSignature)) {
539 $signature = call_user_func($this->processClassSignature, $reflectionClass, $signature);
546 * @param \ReflectionClass $reflectionClass
550 protected function documentClassDocBlock(\ReflectionClass $reflectionClass)
552 if ($this->processClassDocBlock === false) {
555 $doc = self::indentDoc($reflectionClass->getDocComment());
556 if (is_callable($this->processClassDocBlock)) {
557 $doc = call_user_func($this->processClassDocBlock, $reflectionClass, $doc);
563 * @param \ReflectionMethod $reflectedMethod
567 protected function documentMethod(\ReflectionMethod $reflectedMethod)
569 if ($this->processMethod === false) {
572 if (is_callable($this->filterMethods)) {
573 $ret = call_user_func($this->filterMethods, $reflectedMethod);
578 if (!$reflectedMethod->isPublic()) {
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);
593 * @param \ReflectionProperty $reflectedProperty
597 protected function documentProperty(\ReflectionProperty $reflectedProperty)
599 if ($this->processProperty === false) {
602 if (is_callable($this->filterProperties)) {
603 $ret = call_user_func($this->filterProperties, $reflectedProperty);
608 if (!$reflectedProperty->isPublic()) {
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);
622 * @param \ReflectionProperty $reflectedProperty
626 protected function documentPropertySignature(\ReflectionProperty $reflectedProperty)
628 if ($this->processPropertySignature === false) {
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);
640 * @param \ReflectionProperty $reflectedProperty
644 protected function documentPropertyDocBlock(\ReflectionProperty $reflectedProperty)
646 if ($this->processPropertyDocBlock === false) {
649 $propertyDoc = $reflectedProperty->getDocComment();
652 $parent = $reflectedProperty->getDeclaringClass();
653 while ($parent = $parent->getParentClass()) {
654 if ($parent->hasProperty($reflectedProperty->name)) {
655 $propertyDoc = $parent->getProperty($reflectedProperty->name)->getDocComment();
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);
664 return ltrim($propertyDoc);
668 * @param \ReflectionParameter $param
672 protected function documentParam(\ReflectionParameter $param)
675 if ($param->isArray()) {
678 if ($param->isCallable()) {
679 $text .= 'callable ';
681 $text .= '$' . $param->name;
682 if ($param->isDefaultValueAvailable()) {
683 if ($param->allowsNull()) {
686 $text .= ' = ' . str_replace("\n", ' ', print_r($param->getDefaultValue(), true));
699 public static function indentDoc($doc, $indent = 3)
707 function ($line) use ($indent) {
708 return substr($line, $indent);
716 * @param \ReflectionMethod $reflectedMethod
720 protected function documentMethodSignature(\ReflectionMethod $reflectedMethod)
722 if ($this->processMethodSignature === false) {
725 $modifiers = implode(' ', \Reflection::getModifierNames($reflectedMethod->getModifiers()));
730 return $this->documentParam($p);
732 $reflectedMethod->getParameters()
735 $signature = "#### *$modifiers* {$reflectedMethod->name}($params)";
736 if (is_callable($this->processMethodSignature)) {
737 $signature = call_user_func($this->processMethodSignature, $reflectedMethod, $signature);
743 * @param \ReflectionMethod $reflectedMethod
747 protected function documentMethodDocBlock(\ReflectionMethod $reflectedMethod)
749 if ($this->processMethodDocBlock === false) {
752 $methodDoc = $reflectedMethod->getDocComment();
755 $parent = $reflectedMethod->getDeclaringClass();
756 while ($parent = $parent->getParentClass()) {
757 if ($parent->hasMethod($reflectedMethod->name)) {
758 $methodDoc = $parent->getMethod($reflectedMethod->name)->getDocComment();
762 // take from interface
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();
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);