taskGenDoc('models.md') * ->docClass('Model\User') // take class Model\User * ->docClass('Model\Post') // take class Model\Post * ->filterMethods(function(\ReflectionMethod $r) { * return $r->isPublic() or $r->isProtected(); // process public and protected methods * })->processClass(function(\ReflectionClass $r, $text) { * return "Class ".$r->getName()."\n\n$text\n\n###Methods\n"; * })->run(); * ``` * * By default this task generates a documentation for each public method of a class, interface or trait. * It combines method signature with a docblock. Both can be post-processed. * * ``` php * taskGenDoc('models.md') * ->docClass('Model\User') * ->processClassSignature(false) // false can be passed to not include class signature * ->processClassDocBlock(function(\ReflectionClass $r, $text) { * return "[This is part of application model]\n" . $text; * })->processMethodSignature(function(\ReflectionMethod $r, $text) { * return "#### {$r->name}()"; * })->processMethodDocBlock(function(\ReflectionMethod $r, $text) { * return strpos($r->name, 'save')===0 ? "[Saves to the database]\n" . $text : $text; * })->run(); * ``` */ class GenerateMarkdownDoc extends BaseTask implements BuilderAwareInterface { use BuilderAwareTrait; /** * @var string[] */ protected $docClass = []; /** * @var callable */ protected $filterMethods; /** * @var callable */ protected $filterClasses; /** * @var callable */ protected $filterProperties; /** * @var callable */ protected $processClass; /** * @var callable|false */ protected $processClassSignature; /** * @var callable|false */ protected $processClassDocBlock; /** * @var callable|false */ protected $processMethod; /** * @var callable|false */ protected $processMethodSignature; /** * @var callable|false */ protected $processMethodDocBlock; /** * @var callable|false */ protected $processProperty; /** * @var callable|false */ protected $processPropertySignature; /** * @var callable|false */ protected $processPropertyDocBlock; /** * @var callable */ protected $reorder; /** * @var callable */ protected $reorderMethods; /** * @todo Unused property. * * @var callable */ protected $reorderProperties; /** * @var string */ protected $filename; /** * @var string */ protected $prepend = ""; /** * @var string */ protected $append = ""; /** * @var string */ protected $text; /** * @var string[] */ protected $textForClass = []; /** * @param string $filename * * @return static */ public static function init($filename) { return new static($filename); } /** * @param string $filename */ public function __construct($filename) { $this->filename = $filename; } /** * Put a class you want to be documented. * * @param string $item * * @return $this */ public function docClass($item) { $this->docClass[] = $item; return $this; } /** * Using a callback function filter out methods that won't be documented. * * @param callable $filterMethods * * @return $this */ public function filterMethods($filterMethods) { $this->filterMethods = $filterMethods; return $this; } /** * Using a callback function filter out classes that won't be documented. * * @param callable $filterClasses * * @return $this */ public function filterClasses($filterClasses) { $this->filterClasses = $filterClasses; return $this; } /** * Using a callback function filter out properties that won't be documented. * * @param callable $filterProperties * * @return $this */ public function filterProperties($filterProperties) { $this->filterProperties = $filterProperties; return $this; } /** * Post-process class documentation. * * @param callable $processClass * * @return $this */ public function processClass($processClass) { $this->processClass = $processClass; return $this; } /** * Post-process class signature. Provide *false* to skip. * * @param callable|false $processClassSignature * * @return $this */ public function processClassSignature($processClassSignature) { $this->processClassSignature = $processClassSignature; return $this; } /** * Post-process class docblock contents. Provide *false* to skip. * * @param callable|false $processClassDocBlock * * @return $this */ public function processClassDocBlock($processClassDocBlock) { $this->processClassDocBlock = $processClassDocBlock; return $this; } /** * Post-process method documentation. Provide *false* to skip. * * @param callable|false $processMethod * * @return $this */ public function processMethod($processMethod) { $this->processMethod = $processMethod; return $this; } /** * Post-process method signature. Provide *false* to skip. * * @param callable|false $processMethodSignature * * @return $this */ public function processMethodSignature($processMethodSignature) { $this->processMethodSignature = $processMethodSignature; return $this; } /** * Post-process method docblock contents. Provide *false* to skip. * * @param callable|false $processMethodDocBlock * * @return $this */ public function processMethodDocBlock($processMethodDocBlock) { $this->processMethodDocBlock = $processMethodDocBlock; return $this; } /** * Post-process property documentation. Provide *false* to skip. * * @param callable|false $processProperty * * @return $this */ public function processProperty($processProperty) { $this->processProperty = $processProperty; return $this; } /** * Post-process property signature. Provide *false* to skip. * * @param callable|false $processPropertySignature * * @return $this */ public function processPropertySignature($processPropertySignature) { $this->processPropertySignature = $processPropertySignature; return $this; } /** * Post-process property docblock contents. Provide *false* to skip. * * @param callable|false $processPropertyDocBlock * * @return $this */ public function processPropertyDocBlock($processPropertyDocBlock) { $this->processPropertyDocBlock = $processPropertyDocBlock; return $this; } /** * Use a function to reorder classes. * * @param callable $reorder * * @return $this */ public function reorder($reorder) { $this->reorder = $reorder; return $this; } /** * Use a function to reorder methods in class. * * @param callable $reorderMethods * * @return $this */ public function reorderMethods($reorderMethods) { $this->reorderMethods = $reorderMethods; return $this; } /** * @param callable $reorderProperties * * @return $this */ public function reorderProperties($reorderProperties) { $this->reorderProperties = $reorderProperties; return $this; } /** * @param string $filename * * @return $this */ public function filename($filename) { $this->filename = $filename; return $this; } /** * Inserts text at the beginning of markdown file. * * @param string $prepend * * @return $this */ public function prepend($prepend) { $this->prepend = $prepend; return $this; } /** * Inserts text at the end of markdown file. * * @param string $append * * @return $this */ public function append($append) { $this->append = $append; return $this; } /** * @param string $text * * @return $this */ public function text($text) { $this->text = $text; return $this; } /** * @param string $item * * @return $this */ public function textForClass($item) { $this->textForClass[] = $item; return $this; } /** * {@inheritdoc} */ public function run() { foreach ($this->docClass as $class) { $this->printTaskInfo("Processing {class}", ['class' => $class]); $this->textForClass[$class] = $this->documentClass($class); } if (is_callable($this->reorder)) { $this->printTaskInfo("Applying reorder function"); call_user_func_array($this->reorder, [$this->textForClass]); } $this->text = implode("\n", $this->textForClass); /** @var \Robo\Result $result */ $result = $this->collectionBuilder()->taskWriteToFile($this->filename) ->line($this->prepend) ->text($this->text) ->line($this->append) ->run(); $this->printTaskSuccess('{filename} created. {class-count} classes documented', ['filename' => $this->filename, 'class-count' => count($this->docClass)]); return new Result($this, $result->getExitCode(), $result->getMessage(), $this->textForClass); } /** * @param string $class * * @return null|string */ protected function documentClass($class) { if (!class_exists($class) && !trait_exists($class)) { return ""; } $refl = new \ReflectionClass($class); if (is_callable($this->filterClasses)) { $ret = call_user_func($this->filterClasses, $refl); if (!$ret) { return; } } $doc = $this->documentClassSignature($refl); $doc .= "\n" . $this->documentClassDocBlock($refl); $doc .= "\n"; if (is_callable($this->processClass)) { $doc = call_user_func($this->processClass, $refl, $doc); } $properties = []; foreach ($refl->getProperties() as $reflProperty) { $properties[] = $this->documentProperty($reflProperty); } $properties = array_filter($properties); $doc .= implode("\n", $properties); $methods = []; foreach ($refl->getMethods() as $reflMethod) { $methods[$reflMethod->name] = $this->documentMethod($reflMethod); } if (is_callable($this->reorderMethods)) { call_user_func_array($this->reorderMethods, [&$methods]); } $methods = array_filter($methods); $doc .= implode("\n", $methods)."\n"; return $doc; } /** * @param \ReflectionClass $reflectionClass * * @return string */ protected function documentClassSignature(\ReflectionClass $reflectionClass) { if ($this->processClassSignature === false) { return ""; } $signature = "## {$reflectionClass->name}\n\n"; if ($parent = $reflectionClass->getParentClass()) { $signature .= "* *Extends* `{$parent->name}`"; } $interfaces = $reflectionClass->getInterfaceNames(); if (count($interfaces)) { $signature .= "\n* *Implements* `" . implode('`, `', $interfaces) . '`'; } $traits = $reflectionClass->getTraitNames(); if (count($traits)) { $signature .= "\n* *Uses* `" . implode('`, `', $traits) . '`'; } if (is_callable($this->processClassSignature)) { $signature = call_user_func($this->processClassSignature, $reflectionClass, $signature); } return $signature; } /** * @param \ReflectionClass $reflectionClass * * @return string */ protected function documentClassDocBlock(\ReflectionClass $reflectionClass) { if ($this->processClassDocBlock === false) { return ""; } $doc = self::indentDoc($reflectionClass->getDocComment()); if (is_callable($this->processClassDocBlock)) { $doc = call_user_func($this->processClassDocBlock, $reflectionClass, $doc); } return $doc; } /** * @param \ReflectionMethod $reflectedMethod * * @return string */ protected function documentMethod(\ReflectionMethod $reflectedMethod) { if ($this->processMethod === false) { return ""; } if (is_callable($this->filterMethods)) { $ret = call_user_func($this->filterMethods, $reflectedMethod); if (!$ret) { return ""; } } else { if (!$reflectedMethod->isPublic()) { return ""; } } $signature = $this->documentMethodSignature($reflectedMethod); $docblock = $this->documentMethodDocBlock($reflectedMethod); $methodDoc = "$signature $docblock"; if (is_callable($this->processMethod)) { $methodDoc = call_user_func($this->processMethod, $reflectedMethod, $methodDoc); } return $methodDoc; } /** * @param \ReflectionProperty $reflectedProperty * * @return string */ protected function documentProperty(\ReflectionProperty $reflectedProperty) { if ($this->processProperty === false) { return ""; } if (is_callable($this->filterProperties)) { $ret = call_user_func($this->filterProperties, $reflectedProperty); if (!$ret) { return ""; } } else { if (!$reflectedProperty->isPublic()) { return ""; } } $signature = $this->documentPropertySignature($reflectedProperty); $docblock = $this->documentPropertyDocBlock($reflectedProperty); $propertyDoc = $signature . $docblock; if (is_callable($this->processProperty)) { $propertyDoc = call_user_func($this->processProperty, $reflectedProperty, $propertyDoc); } return $propertyDoc; } /** * @param \ReflectionProperty $reflectedProperty * * @return string */ protected function documentPropertySignature(\ReflectionProperty $reflectedProperty) { if ($this->processPropertySignature === false) { return ""; } $modifiers = implode(' ', \Reflection::getModifierNames($reflectedProperty->getModifiers())); $signature = "#### *$modifiers* {$reflectedProperty->name}"; if (is_callable($this->processPropertySignature)) { $signature = call_user_func($this->processPropertySignature, $reflectedProperty, $signature); } return $signature; } /** * @param \ReflectionProperty $reflectedProperty * * @return string */ protected function documentPropertyDocBlock(\ReflectionProperty $reflectedProperty) { if ($this->processPropertyDocBlock === false) { return ""; } $propertyDoc = $reflectedProperty->getDocComment(); // take from parent if (!$propertyDoc) { $parent = $reflectedProperty->getDeclaringClass(); while ($parent = $parent->getParentClass()) { if ($parent->hasProperty($reflectedProperty->name)) { $propertyDoc = $parent->getProperty($reflectedProperty->name)->getDocComment(); } } } $propertyDoc = self::indentDoc($propertyDoc, 7); $propertyDoc = preg_replace("~^@(.*?)([$\s])~", ' * `$1` $2', $propertyDoc); // format annotations if (is_callable($this->processPropertyDocBlock)) { $propertyDoc = call_user_func($this->processPropertyDocBlock, $reflectedProperty, $propertyDoc); } return ltrim($propertyDoc); } /** * @param \ReflectionParameter $param * * @return string */ protected function documentParam(\ReflectionParameter $param) { $text = ""; if ($param->isArray()) { $text .= 'array '; } if ($param->isCallable()) { $text .= 'callable '; } $text .= '$' . $param->name; if ($param->isDefaultValueAvailable()) { if ($param->allowsNull()) { $text .= ' = null'; } else { $text .= ' = ' . str_replace("\n", ' ', print_r($param->getDefaultValue(), true)); } } return $text; } /** * @param string $doc * @param int $indent * * @return string */ public static function indentDoc($doc, $indent = 3) { if (!$doc) { return $doc; } return implode( "\n", array_map( function ($line) use ($indent) { return substr($line, $indent); }, explode("\n", $doc) ) ); } /** * @param \ReflectionMethod $reflectedMethod * * @return string */ protected function documentMethodSignature(\ReflectionMethod $reflectedMethod) { if ($this->processMethodSignature === false) { return ""; } $modifiers = implode(' ', \Reflection::getModifierNames($reflectedMethod->getModifiers())); $params = implode( ', ', array_map( function ($p) { return $this->documentParam($p); }, $reflectedMethod->getParameters() ) ); $signature = "#### *$modifiers* {$reflectedMethod->name}($params)"; if (is_callable($this->processMethodSignature)) { $signature = call_user_func($this->processMethodSignature, $reflectedMethod, $signature); } return $signature; } /** * @param \ReflectionMethod $reflectedMethod * * @return string */ protected function documentMethodDocBlock(\ReflectionMethod $reflectedMethod) { if ($this->processMethodDocBlock === false) { return ""; } $methodDoc = $reflectedMethod->getDocComment(); // take from parent if (!$methodDoc) { $parent = $reflectedMethod->getDeclaringClass(); while ($parent = $parent->getParentClass()) { if ($parent->hasMethod($reflectedMethod->name)) { $methodDoc = $parent->getMethod($reflectedMethod->name)->getDocComment(); } } } // take from interface if (!$methodDoc) { $interfaces = $reflectedMethod->getDeclaringClass()->getInterfaces(); foreach ($interfaces as $interface) { $i = new \ReflectionClass($interface->name); if ($i->hasMethod($reflectedMethod->name)) { $methodDoc = $i->getMethod($reflectedMethod->name)->getDocComment(); break; } } } $methodDoc = self::indentDoc($methodDoc, 7); $methodDoc = preg_replace("~^@(.*?) ([$\s])~m", ' * `$1` $2', $methodDoc); // format annotations if (is_callable($this->processMethodDocBlock)) { $methodDoc = call_user_func($this->processMethodDocBlock, $reflectedMethod, $methodDoc); } return $methodDoc; } }