Refactoring

This commit is contained in:
gamonoid
2017-09-03 20:39:22 +02:00
parent af40881847
commit a7274d3cfd
5075 changed files with 238202 additions and 16291 deletions

View File

@@ -0,0 +1,202 @@
<?php
namespace PHPDocsMD;
/**
* Object describing a class or an interface
* @package PHPDocsMD
*/
class ClassEntity extends CodeEntity {
/**
* @var \PHPDocsMD\FunctionEntity[]
*/
private $functions = [];
/**
* @var bool
*/
private $isInterface = false;
/**
* @var bool
*/
private $abstract = false;
/**
* @var bool
*/
private $hasIgnoreTag = false;
/**
* @var string
*/
private $extends = '';
/**
* @var array
*/
private $interfaces = [];
/**
* @var bool
*/
private $isNative;
/**
* @param bool $toggle
* @return bool
*/
public function isAbstract($toggle=null)
{
if ( $toggle === null ) {
return $this->abstract;
} else {
return $this->abstract = (bool)$toggle;
}
}
/**
* @param bool $toggle
* @return bool
*/
public function hasIgnoreTag($toggle=null)
{
if( $toggle === null ) {
return $this->hasIgnoreTag;
} else {
return $this->hasIgnoreTag = (bool)$toggle;
}
}
/**
* @param bool $toggle
* @return bool
*/
public function isInterface($toggle=null)
{
if( $toggle === null ) {
return $this->isInterface;
} else {
return $this->isInterface = (bool)$toggle;
}
}
/**
* @param bool $toggle
* @return bool
*/
public function isNative($toggle=null)
{
if( $toggle === null ) {
return $this->isNative;
} else {
return $this->isNative = (bool)$toggle;
}
}
/**
* @param string $extends
*/
public function setExtends($extends)
{
$this->extends = Utils::sanitizeClassName($extends);
}
/**
* @return string
*/
public function getExtends()
{
return $this->extends;
}
/**
* @param \PHPDocsMD\FunctionEntity[] $functions
*/
public function setFunctions(array $functions)
{
$this->functions = $functions;
}
/**
* @param array $implements
*/
public function setInterfaces(array $implements)
{
$this->interfaces = [];
foreach($implements as $interface) {
$this->interfaces[] = Utils::sanitizeClassName($interface);
}
}
/**
* @return array
*/
public function getInterfaces()
{
return $this->interfaces;
}
/**
* @return \PHPDocsMD\FunctionEntity[]
*/
public function getFunctions()
{
return $this->functions;
}
/**
* @param string $name
*/
function setName($name)
{
parent::setName(Utils::sanitizeClassName($name));
}
/**
* Check whether this object is referring to given class name or object instance
* @param string|object $class
* @return bool
*/
function isSame($class)
{
$className = is_object($class) ? get_class($class) : $class;
return Utils::sanitizeClassName($className) == $this->getName();
}
/**
* Generate a title describing the class this object is referring to
* @param string $format
* @return string
*/
function generateTitle($format='%label%: %name% %extra%')
{
$translate = [
'%label%' => $this->isInterface() ? 'Interface' : 'Class',
'%name%' => substr_count($this->getName(), '\\') == 1 ? substr($this->getName(), 1) : $this->getName(),
'%extra%' => ''
];
if( strpos($format, '%label%') === false ) {
if( $this->isInterface() )
$translate['%extra%'] = '(interface)';
elseif( $this->isAbstract() )
$translate['%extra%'] = '(abstract)';
} else {
$translate['%extra%'] = $this->isAbstract() && !$this->isInterface() ? '(abstract)' : '';
}
return trim(strtr($format, $translate));
}
/**
* Generates an anchor link out of the generated title (see generateTitle)
* @return string
*/
function generateAnchor()
{
$title = $this->generateTitle();
return strtolower(str_replace([':', ' ', '\\', '(', ')'], ['', '-', '', '', ''], $title));
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace PHPDocsMD;
/**
* Class capable of creating ClassEntity objects
* @package PHPDocsMD
*/
class ClassEntityFactory
{
/**
* @var DocInfoExtractor
*/
private $docInfoExtractor;
/**
* @param DocInfoExtractor $docInfoExtractor
*/
public function __construct(DocInfoExtractor $docInfoExtractor)
{
$this->docInfoExtractor = $docInfoExtractor;
}
public function create(\ReflectionClass $reflection)
{
$class = new ClassEntity();
$docInfo = $this->docInfoExtractor->extractInfo($reflection);
$this->docInfoExtractor->applyInfoToEntity($reflection, $docInfo, $class);
$class->isInterface($reflection->isInterface());
$class->isAbstract($reflection->isAbstract());
$class->setInterfaces(array_keys($reflection->getInterfaces()));
$class->hasIgnoreTag($docInfo->shouldBeIgnored());
if ($reflection->getParentClass()) {
$class->setExtends($reflection->getParentClass()->getName());
}
return $class;
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace PHPDocsMD;
/**
* Object describing a piece of code
* @package PHPDocsMD
*/
class CodeEntity {
/**
* @var string
*/
private $name='';
/**
* @var string
*/
private $description = '';
/**
* @var bool
*/
private $isDeprecated = false;
/**
* @var string
*/
private $deprecationMessage = '';
/**
* @var string
*/
private $example = '';
/**
* @param bool $toggle
* @return void|bool
*/
public function isDeprecated($toggle=null)
{
if( $toggle === null ) {
return $this->isDeprecated;
} else {
return $this->isDeprecated = (bool)$toggle;
}
}
/**
* @param string $description
*/
public function setDescription($description)
{
$this->description = $description;
}
/**
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* @param string $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string $deprecationMessage
*/
public function setDeprecationMessage($deprecationMessage)
{
$this->deprecationMessage = $deprecationMessage;
}
/**
* @return string
*/
public function getDeprecationMessage()
{
return $this->deprecationMessage;
}
/**
* @param string $example
*/
public function setExample($example)
{
$this->example = $example;
}
/**
* @return string
*/
public function getExample()
{
return $this->example;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace PHPDocsMD\Console;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Command line interface used to extract markdown-formatted documentation from classes
* @package PHPDocsMD\Console
*/
class CLI extends Application {
public function __construct()
{
$json = json_decode(file_get_contents(__DIR__.'/../../../composer.json'));
parent::__construct('PHP Markdown Documentation Generator', $json->version);
}
/**
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Input\OutputInterface $output
* @return int
*/
public function run(InputInterface $input=null, OutputInterface $output=null)
{
$this->add(new PHPDocsMDCommand());
return parent::run($input, $output);
}
}

View File

@@ -0,0 +1,288 @@
<?php
namespace PHPDocsMD\Console;
use PHPDocsMD\MDTableGenerator;
use PHPDocsMD\Reflector;
use PHPDocsMD\Utils;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Console command used to extract markdown-formatted documentation from classes
* @package PHPDocsMD\Console
*/
class PHPDocsMDCommand extends \Symfony\Component\Console\Command\Command {
const ARG_CLASS = 'class';
const OPT_BOOTSTRAP = 'bootstrap';
const OPT_IGNORE = 'ignore';
/**
* @var array
*/
private $memory = [];
/**
* @param $name
* @return \PHPDocsMD\ClassEntity
*/
private function getClassEntity($name) {
if( !isset($this->memory[$name]) ) {
$reflector = new Reflector($name);
$this->memory[$name] = $reflector->getClassEntity();
}
return $this->memory[$name];
}
protected function configure()
{
$this
->setName('generate')
->setDescription('Get docs for given class/source directory)')
->addArgument(
self::ARG_CLASS,
InputArgument::REQUIRED,
'Class or source directory'
)
->addOption(
self::OPT_BOOTSTRAP,
'b',
InputOption::VALUE_REQUIRED,
'File to be included before generating documentation'
)
->addOption(
self::OPT_IGNORE,
'i',
InputOption::VALUE_REQUIRED,
'Directories to ignore',
''
);
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int|null
* @throws \InvalidArgumentException
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$classes = $input->getArgument(self::ARG_CLASS);
$bootstrap = $input->getOption(self::OPT_BOOTSTRAP);
$ignore = explode(',', $input->getOption(self::OPT_IGNORE));
$requestingOneClass = false;
if( $bootstrap ) {
require_once strpos($bootstrap,'/') === 0 ? $bootstrap : getcwd().'/'.$bootstrap;
}
$classCollection = [];
if( strpos($classes, ',') !== false ) {
foreach(explode(',', $classes) as $class) {
if( class_exists($class) || interface_exists($class) )
$classCollection[0][] = $class;
}
}
elseif( class_exists($classes) || interface_exists($classes) ) {
$classCollection[] = array($classes);
$requestingOneClass = true;
} elseif( is_dir($classes) ) {
$classCollection = $this->findClassesInDir($classes, [], $ignore);
} else {
throw new \InvalidArgumentException('Given input is neither a class nor a source directory');
}
$tableGenerator = new MDTableGenerator();
$tableOfContent = [];
$body = [];
$classLinks = [];
foreach($classCollection as $ns => $classes) {
foreach($classes as $className) {
$class = $this->getClassEntity($className);
if( $class->hasIgnoreTag() )
continue;
// Add to tbl of contents
$tableOfContent[] = sprintf('- [%s](#%s)', $class->generateTitle('%name% %extra%'), $class->generateAnchor());
$classLinks[$class->getName()] = '#'.$class->generateAnchor();
// generate function table
$tableGenerator->openTable();
$tableGenerator->doDeclareAbstraction(!$class->isInterface());
foreach($class->getFunctions() as $func) {
if ($func->isReturningNativeClass()) {
$classLinks[$func->getReturnType()] = 'http://php.net/manual/en/class.'.
strtolower(str_replace(array('[]', '\\'), '', $func->getReturnType())).
'.php';
}
foreach($func->getParams() as $param) {
if ($param->getNativeClassType()) {
$classLinks[$param->getNativeClassType()] = 'http://php.net/manual/en/class.'.
strtolower(str_replace(array('[]', '\\'), '', $param->getNativeClassType())).
'.php';
}
}
$tableGenerator->addFunc($func);
}
$docs = ($requestingOneClass ? '':'<hr /> ').PHP_EOL;
if( $class->isDeprecated() ) {
$docs .= '### <strike>'.$class->generateTitle().'</strike>'.PHP_EOL.PHP_EOL.
'> **DEPRECATED** '.$class->getDeprecationMessage().PHP_EOL.PHP_EOL;
}
else {
$docs .= '### '.$class->generateTitle().PHP_EOL.PHP_EOL;
if( $class->getDescription() )
$docs .= '> '.$class->getDescription().PHP_EOL.PHP_EOL;
}
if( $example = $class->getExample() ) {
$docs .= '###### Example' . PHP_EOL . MDTableGenerator::formatExampleComment($example) .PHP_EOL.PHP_EOL;
}
$docs .= $tableGenerator->getTable().PHP_EOL;
if( $class->getExtends() ) {
$link = $class->getExtends();
if( $anchor = $this->getAnchorFromClassCollection($classCollection, $class->getExtends()) ) {
$link = sprintf('[%s](#%s)', $link, $anchor);
}
$docs .= PHP_EOL.'*This class extends '.$link.'*'.PHP_EOL;
}
if( $interfaces = $class->getInterfaces() ) {
$interfaceNames = [];
foreach($interfaces as $interface) {
$anchor = $this->getAnchorFromClassCollection($classCollection, $interface);
$interfaceNames[] = $anchor ? sprintf('[%s](#%s)', $interface, $anchor) : $interface;
}
$docs .= PHP_EOL.'*This class implements '.implode(', ', $interfaceNames).'*'.PHP_EOL;
}
$body[] = $docs;
}
}
if( empty($tableOfContent) ) {
throw new \InvalidArgumentException('No classes found');
} elseif( !$requestingOneClass ) {
$output->writeln('## Table of contents'.PHP_EOL);
$output->writeln(implode(PHP_EOL, $tableOfContent));
}
// Convert references to classes into links
asort($classLinks);
$classLinks = array_reverse($classLinks, true);
$docString = implode(PHP_EOL, $body);
foreach($classLinks as $className => $url) {
$link = sprintf('[%s](%s)', $className, $url);
$find = array('<em>'.$className, '/'.$className);
$replace = array('<em>'.$link, '/'.$link);
$docString = str_replace($find, $replace, $docString);
}
$output->writeln(PHP_EOL.$docString);
}
/**
* @param $coll
* @param $find
* @return bool|string
*/
private function getAnchorFromClassCollection($coll, $find)
{
foreach($coll as $ns => $classes) {
foreach($classes as $className) {
if( $className == $find ) {
return $this->getClassEntity($className)->generateAnchor();
}
}
}
return false;
}
/**
* @param $file
* @return array
*/
private function findClassInFile($file)
{
$ns = '';
$class = false;
foreach(explode(PHP_EOL, file_get_contents($file)) as $line) {
if ( strpos($line, '*') === false ) {
if( strpos($line, 'namespace') !== false ) {
$ns = trim(current(array_slice(explode('namespace', $line), 1)), '; ');
$ns = Utils::sanitizeClassName($ns);
} elseif( strpos($line, 'class') !== false ) {
$class = $this->extractClassNameFromLine('class', $line);
break;
} elseif( strpos($line, 'interface') !== false ) {
$class = $this->extractClassNameFromLine('interface', $line);
break;
}
}
}
return $class ? array($ns, $ns .'\\'. $class) : array(false, false);
}
/**
* @param string $type
* @param string $line
* @return string
*/
function extractClassNameFromLine($type, $line)
{
$class = trim(current(array_slice(explode($type, $line), 1)), '; ');
return trim(current(explode(' ', $class)));
}
/**
* @param $dir
* @param array $collection
* @param array $ignores
* @return array
*/
private function findClassesInDir($dir, $collection=[], $ignores=[])
{
foreach(new \FilesystemIterator($dir) as $f) {
/** @var \SplFileInfo $f */
if( $f->isFile() && !$f->isLink() ) {
list($ns, $className) = $this->findClassInFile($f->getRealPath());
if( $className && (class_exists($className, true) || interface_exists($className)) ) {
$collection[$ns][] = $className;
}
} elseif( $f->isDir() && !$f->isLink() && !$this->shouldIgnoreDirectory($f->getFilename(), $ignores) ) {
$collection = $this->findClassesInDir($f->getRealPath(), $collection);
}
}
ksort($collection);
return $collection;
}
/**
* @param $dirName
* @param $ignores
* @return bool
*/
private function shouldIgnoreDirectory($dirName, $ignores) {
foreach($ignores as $dir) {
$dir = trim($dir);
if( !empty($dir) && substr($dirName, -1 * strlen($dir)) == $dir ) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace PHPDocsMD;
/**
* Class containing information about a function/class that's being made
* available via a comment block
*
* @package PHPDocsMD
*/
class DocInfo
{
/**
* @var array
*/
private $data = [];
/**
* @param array $data
*/
public function __construct(array $data)
{
$this->data = array_merge([
'return' => '',
'params' => [],
'description' => '',
'example' => false,
'deprecated' => false
], $data);
}
/**
* @return string
*/
public function getReturnType()
{
return $this->data['return'];
}
/**
* @return array
*/
public function getParameters()
{
return $this->data['params'];
}
/**
* @param string $name
* @return array
*/
public function getParameterInfo($name)
{
if (isset($this->data['params'][$name])) {
return $this->data['params'][$name];
}
return [];
}
/**
* @return string
*/
public function getExample()
{
return $this->data['example'];
}
/**
* @return string
*/
public function getDescription()
{
return $this->data['description'];
}
/**
* @return string
*/
public function getDeprecationMessage()
{
return $this->data['deprecated'];
}
/**
* @return bool
*/
public function shouldInheritDoc()
{
return isset($this->data['inheritDoc']) || isset($this->data['inheritdoc']);
}
/**
* @return bool
*/
public function shouldBeIgnored()
{
return isset($this->data['ignore']);
}
}

View File

@@ -0,0 +1,174 @@
<?php
namespace PHPDocsMD;
/**
* Class that can extract information from a function/class comment
* @package PHPDocsMD
*/
class DocInfoExtractor
{
/**
* @param \ReflectionClass|\ReflectionMethod $reflection
* @return DocInfo
*/
public function extractInfo($reflection)
{
$comment = $this->getCleanDocComment($reflection);
$data = $this->extractInfoFromComment($comment, $reflection);
return new DocInfo($data);
}
/**
* @param \ReflectionClass|\ReflectionMethod $reflection
* @param DocInfo $docInfo
* @param CodeEntity $code
*/
public function applyInfoToEntity($reflection, DocInfo $docInfo, CodeEntity $code)
{
$code->setName($reflection->getName());
$code->setDescription($docInfo->getDescription());
$code->setExample($docInfo->getExample());
if ($docInfo->getDeprecationMessage()) {
$code->isDeprecated(true);
$code->setDeprecationMessage($docInfo->getDeprecationMessage());
}
}
/**
* @param \ReflectionClass $reflection
* @return string
*/
private function getCleanDocComment($reflection)
{
$comment = str_replace(['/*', '*/'], '', $reflection->getDocComment());
return trim(trim(preg_replace('/([\s|^]\*\s)/', '', $comment)), '*');
}
/**
* @param string $comment
* @param string $current_tag
* @param \ReflectionMethod|\ReflectionClass $reflection
* @return array
*/
private function extractInfoFromComment($comment, $reflection, $current_tag='description')
{
$currentNamespace = $this->getNameSpace($reflection);
$tags = [$current_tag=>''];
foreach(explode(PHP_EOL, $comment) as $line) {
if( $current_tag != 'example' )
$line = trim($line);
$words = $this->getWordsFromLine($line);
if( empty($words) )
continue;
if( strpos($words[0], '@') === false ) {
// Append to tag
$joinWith = $current_tag == 'example' ? PHP_EOL : ' ';
$tags[$current_tag] .= $joinWith . $line;
}
elseif( $words[0] == '@param' ) {
// Get parameter declaration
if( $paramData = $this->figureOutParamDeclaration($words, $currentNamespace) ) {
list($name, $data) = $paramData;
$tags['params'][$name] = $data;
}
}
else {
// Start new tag
$current_tag = substr($words[0], 1);
array_splice($words, 0 ,1);
if( empty($tags[$current_tag]) ) {
$tags[$current_tag] = '';
}
$tags[$current_tag] .= trim(join(' ', $words));
}
}
foreach($tags as $name => $val) {
if( is_array($val) ) {
foreach($val as $subName=>$subVal) {
if( is_string($subVal) )
$tags[$name][$subName] = trim($subVal);
}
} else {
$tags[$name] = trim($val);
}
}
return $tags;
}
/**
* @param \ReflectionClass|\ReflectionMethod $reflection
* @return string
*/
private function getNameSpace($reflection)
{
if ($reflection instanceof \ReflectionClass) {
return $reflection->getNamespaceName();
} else {
return $reflection->getDeclaringClass()->getNamespaceName();
}
}
/**
* @param $line
* @return array
*/
private function getWordsFromLine($line)
{
$words = [];
foreach(explode(' ', trim($line)) as $w) {
if( !empty($w) ) {
$words[] = $w;
}
}
return $words;
}
/**
* @param $words
* @param $currentNameSpace
* @return array|bool
*/
private function figureOutParamDeclaration($words, $currentNameSpace)
{
$description = '';
$type = '';
$name = '';
if (isset($words[1]) && strpos($words[1], '$') === 0) {
$name = $words[1];
$type = 'mixed';
array_splice($words, 0, 2);
} elseif (isset($words[2])) {
$name = $words[2];
$type = $words[1];
array_splice($words, 0, 3);
}
if (!empty($name)) {
$name = current(explode('=', $name));
if( count($words) > 1 ) {
$description = join(' ', $words);
}
$type = Utils::sanitizeDeclaration($type, $currentNameSpace);
$data = [
'description' => $description,
'name' => $name,
'type' => $type,
'default' => false
];
return [$name, $data];
}
return false;
}
}

View File

@@ -0,0 +1,154 @@
<?php
namespace PHPDocsMD;
/**
* Object describing a function
* @package PHPDocsMD
*/
class FunctionEntity extends CodeEntity {
/**
* @var \PHPDocsMD\ParamEntity[]
*/
private $params = [];
/**
* @var string
*/
private $returnType = 'void';
/**
* @var string
*/
private $visibility = 'public';
/**
* @var bool
*/
private $abstract = false;
/**
* @var bool
*/
private $isStatic = false;
/**
* @var string
*/
private $class = '';
/**
* @var bool
*/
private $isReturningNativeClass = false;
/**
* @param bool $toggle
*/
public function isStatic($toggle=null)
{
if ( $toggle === null ) {
return $this->isStatic;
} else {
return $this->isStatic = (bool)$toggle;
}
}
/**
* @param bool $toggle
*/
public function isAbstract($toggle=null)
{
if ( $toggle === null ) {
return $this->abstract;
} else {
return $this->abstract = (bool)$toggle;
}
}
/**
* @param bool $toggle
*/
public function isReturningNativeClass($toggle=null)
{
if ( $toggle === null ) {
return $this->isReturningNativeClass;
} else {
return $this->isReturningNativeClass = (bool)$toggle;
}
}
/**
* @return bool
*/
public function hasParams()
{
return !empty($this->params);
}
/**
* @param \PHPDocsMD\ParamEntity[] $params
*/
public function setParams(array $params)
{
$this->params = $params;
}
/**
* @return \PHPDocsMD\ParamEntity[]
*/
public function getParams()
{
return $this->params;
}
/**
* @param string $returnType
*/
public function setReturnType($returnType)
{
$this->returnType = $returnType;
}
/**
* @return string
*/
public function getReturnType()
{
return $this->returnType;
}
/**
* @param string $visibility
*/
public function setVisibility($visibility)
{
$this->visibility = $visibility;
}
/**
* @return string
*/
public function getVisibility()
{
return $this->visibility;
}
/**
* @param string $class
*/
public function setClass($class)
{
$this->class = $class;
}
/**
* @return string
*/
public function getClass()
{
return $this->class;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace PHPDocsMD;
/**
* Find a specific function in a class or an array of classes
* @package PHPDocsMD
*/
class FunctionFinder
{
/**
* @var array
*/
private $cache = [];
/**
* @param string $methodName
* @param array $classes
* @return bool|FunctionEntity
*/
public function findInClasses($methodName, $classes)
{
foreach ($classes as $className) {
$function = $this->find($methodName, $className);
if (false !== $function) {
return $function;
}
}
return false;
}
/**
* @param string $methodName
* @param string $className
* @return bool|FunctionEntity
*/
public function find($methodName, $className)
{
if ($className) {
$classEntity = $this->loadClassEntity($className);
$functions = $classEntity->getFunctions();
foreach($functions as $function) {
if($function->getName() == $methodName) {
return $function;
}
}
if($classEntity->getExtends()) {
return $this->find($methodName, $classEntity->getExtends());
}
}
return false;
}
/**
* @param $className
* @return ClassEntity
*/
private function loadClassEntity($className)
{
if (empty($this->cache[$className])) {
$reflector = new Reflector($className, $this);
$this->cache[$className] = $reflector->getClassEntity();
}
return $this->cache[$className];
}
}

View File

@@ -0,0 +1,204 @@
<?php
namespace PHPDocsMD;
/**
* Class that can create a markdown-formatted table describing class functions
* referred to via FunctionEntity objects
*
* @example
* <code>
* <?php
* $generator = new PHPDocsMD\MDTableGenerator();
* $generator->openTable();
* foreach($classEntity->getFunctions() as $func) {
* $generator->addFunc( $func );
* }
* echo $generator->getTable();
* </code>
*
* @package PHPDocsMD
*/
class MDTableGenerator {
/**
* @var string
*/
private $fullClassName = '';
/**
* @var string
*/
private $markdown = '';
/**
* @var array
*/
private $examples = [];
/**
* @var bool
*/
private $appendExamples = true;
/**
* @var bool
*/
private $declareAbstraction = true;
/**
* @param $example
* @return mixed
*/
private static function stripCodeTags($example)
{
if (strpos($example, '<code') !== false) {
$parts = array_slice(explode('</code>', $example), -2);
$example = current($parts);
$parts = array_slice(explode('<code>', $example), 1);
$example = current($parts);
}
return $example;
}
/**
* All example comments found while generating the table will be
* appended to the end of the table. Set $toggle to false to
* prevent this behaviour
*
* @param bool $toggle
*/
function appendExamplesToEndOfTable($toggle)
{
$this->appendExamples = (bool)$toggle;
}
/**
* Begin generating a new markdown-formatted table
*/
function openTable()
{
$this->examples = [];
$this->markdown = ''; // Clear table
$this->declareAbstraction = true;
$this->add('| Visibility | Function |');
$this->add('|:-----------|:---------|');
}
/**
* Toggle whether or not methods being abstract (or part of an interface)
* should be declared as abstract in the table
* @param bool $toggle
*/
function doDeclareAbstraction($toggle) {
$this->declareAbstraction = (bool)$toggle;
}
/**
* Generates a markdown formatted table row with information about given function. Then adds the
* row to the table and returns the markdown formatted string.
*
* @param FunctionEntity $func
* @return string
*/
function addFunc(FunctionEntity $func)
{
$this->fullClassName = $func->getClass();
$str = '<strong>';
if( $this->declareAbstraction && $func->isAbstract() )
$str .= 'abstract ';
$str .= $func->getName().'(';
if( $func->hasParams() ) {
$params = [];
foreach($func->getParams() as $param) {
$paramStr = '<em>'.$param->getType().'</em> <strong>'.$param->getName();
if( $param->getDefault() ) {
$paramStr .= '='.$param->getDefault();
}
$paramStr .= '</strong>';
$params[] = $paramStr;
}
$str .= '</strong>'.implode(', ', $params) .')';
} else {
$str .= ')';
}
$str .= '</strong> : <em>'.$func->getReturnType().'</em>';
if( $func->isDeprecated() ) {
$str = '<strike>'.$str.'</strike>';
$str .= '<br /><em>DEPRECATED - '.$func->getDeprecationMessage().'</em>';
} elseif( $func->getDescription() ) {
$str .= '<br /><em>'.$func->getDescription().'</em>';
}
$str = str_replace(['</strong><strong>', '</strong></strong> '], ['','</strong>'], trim($str));
if( $func->getExample() )
$this->examples[$func->getName()] = $func->getExample();
$firstCol = $func->getVisibility() . ($func->isStatic() ? ' static':'');
$markDown = '| '.$firstCol.' | '.$str.' |';
$this->add($markDown);
return $markDown;
}
/**
* @return string
*/
function getTable()
{
$tbl = trim($this->markdown);
if( $this->appendExamples && !empty($this->examples) ) {
$className = Utils::getClassBaseName($this->fullClassName);
foreach ($this->examples as $funcName => $example) {
$tbl .= sprintf("\n###### Examples of %s::%s()\n%s", $className, $funcName, self::formatExampleComment($example));
}
}
return $tbl;
}
/**
* Create a markdown-formatted code view out of an example comment
* @param string $example
* @return string
*/
public static function formatExampleComment($example)
{
// Remove possible code tag
$example = self::stripCodeTags($example);
if( preg_match('/(\n )/', $example) ) {
$example = preg_replace('/(\n )/', "\n", $example);
}
elseif( preg_match('/(\n )/', $example) ) {
$example = preg_replace('/(\n )/', "\n", $example);
} else {
$example = preg_replace('/(\n )/', "\n", $example);
}
$type = '';
// A very naive analysis of the programming language used in the comment
if( strpos($example, '<?php') !== false ) {
$type = 'php';
}
elseif( strpos($example, 'var ') !== false && strpos($example, '</') === false ) {
$type = 'js';
}
return sprintf("```%s\n%s\n```", $type, trim($example));
}
/**
* @param $str
*/
private function add($str)
{
$this->markdown .= $str .PHP_EOL;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace PHPDocsMD;
/**
* Object describing a function parameter
* @package PHPDocsMD
*/
class ParamEntity extends CodeEntity {
/**
* @var bool
*/
private $default=false;
/**
* @var string
*/
private $type='mixed';
/**
* @param boolean $default
*/
public function setDefault($default)
{
$this->default = $default;
}
/**
* @return boolean
*/
public function getDefault()
{
return $this->default;
}
/**
* @param string $type
*/
public function setType($type)
{
$this->type = $type;
}
/**
* @return string
*/
public function getType()
{
return $this->type;
}
/**
* @return string|null
*/
public function getNativeClassType()
{
foreach(explode('/', $this->type) as $typeDeclaration) {
if (Utils::isNativeClassReference($typeDeclaration)) {
return $typeDeclaration;
}
}
return null;
}
}

View File

@@ -0,0 +1,417 @@
<?php
namespace PHPDocsMD;
use ReflectionMethod;
/**
* Class that can compute ClassEntity objects out of real classes
* @package PHPDocsMD
*/
class Reflector implements ReflectorInterface
{
/**
* @var string
*/
private $className;
/**
* @var FunctionFinder
*/
private $functionFinder;
/**
* @var DocInfoExtractor
*/
private $docInfoExtractor;
/**
* @var ClassEntityFactory
*/
private $classEntityFactory;
/**
* @var UseInspector
*/
private $useInspector;
/**
* @param string $className
* @param FunctionFinder $functionFinder
* @param DocInfoExtractor $docInfoExtractor
* @param UseInspector $useInspector
* @param ClassEntityFactory $classEntityFactory
*/
function __construct(
$className,
FunctionFinder $functionFinder = null,
DocInfoExtractor $docInfoExtractor = null,
UseInspector $useInspector = null,
ClassEntityFactory $classEntityFactory = null
) {
$this->className = $className;
$this->functionFinder = $this->loadIfNull($functionFinder, FunctionFinder::class);
$this->docInfoExtractor = $this->loadIfNull($docInfoExtractor, DocInfoExtractor::class);
$this->useInspector = $this->loadIfNull($useInspector, UseInspector::class);
$this->classEntityFactory = $this->loadIfNull(
$classEntityFactory,
ClassEntityFactory::class,
$this->docInfoExtractor
);
}
private function loadIfNull($obj, $className, $in=null)
{
return is_object($obj) ? $obj : new $className($in);
}
/**
* @return \PHPDocsMD\ClassEntity
*/
function getClassEntity() {
$classReflection = new \ReflectionClass($this->className);
$classEntity = $this->classEntityFactory->create($classReflection);
$classEntity->setFunctions($this->getClassFunctions($classEntity, $classReflection));
return $classEntity;
}
/**
* @param ClassEntity $classEntity
* @param \ReflectionClass $reflectionClass
* @return FunctionEntity[]
*/
private function getClassFunctions(ClassEntity $classEntity, \ReflectionClass $reflectionClass)
{
$classUseStatements = $this->useInspector->getUseStatements($reflectionClass);
$publicFunctions = [];
$protectedFunctions = [];
foreach($reflectionClass->getMethods() as $methodReflection) {
$func = $this->createFunctionEntity(
$methodReflection,
$classEntity,
$classUseStatements
);
if( $func ) {
if( $func->getVisibility() == 'public' ) {
$publicFunctions[$func->getName()] = $func;
} else {
$protectedFunctions[$func->getName()] = $func;
}
}
}
ksort($publicFunctions);
ksort($protectedFunctions);
return array_values(array_merge($publicFunctions, $protectedFunctions));
}
/**
* @param ReflectionMethod $method
* @param ClassEntity $class
* @param array $useStatements
* @return bool|FunctionEntity
*/
protected function createFunctionEntity(ReflectionMethod $method, ClassEntity $class, $useStatements)
{
$func = new FunctionEntity();
$docInfo = $this->docInfoExtractor->extractInfo($method);
$this->docInfoExtractor->applyInfoToEntity($method, $docInfo, $func);
if ($docInfo->shouldInheritDoc()) {
return $this->findInheritedFunctionDeclaration($func, $class);
}
if ($this->shouldIgnoreFunction($docInfo, $method, $class)) {
return false;
}
$returnType = $this->getReturnType($docInfo, $method, $func, $useStatements);
$func->setReturnType($returnType);
$func->setParams($this->getParams($method, $docInfo));
$func->isStatic($method->isStatic());
$func->setVisibility($method->isPublic() ? 'public' : 'protected');
$func->isAbstract($method->isAbstract());
$func->setClass($class->getName());
$func->isReturningNativeClass(Utils::isNativeClassReference($returnType));
return $func;
}
/**
* @param DocInfo $docInfo
* @param ReflectionMethod $method
* @param FunctionEntity $func
* @param array $useStatements
* @return string
*/
private function getReturnType(
DocInfo $docInfo,
ReflectionMethod $method,
FunctionEntity $func,
array $useStatements
) {
$returnType = $docInfo->getReturnType();
if (empty($returnType)) {
$returnType = $this->guessReturnTypeFromFuncName($func->getName());
} elseif(Utils::isClassReference($returnType) && !self::classExists($returnType)) {
$isReferenceToArrayOfObjects = substr($returnType, -2) == '[]' ? '[]':'';
if ($isReferenceToArrayOfObjects) {
$returnType = substr($returnType, 0, strlen($returnType)-2);
}
$className = $this->stripAwayNamespace($returnType);
foreach ($useStatements as $usedClass) {
if ($this->stripAwayNamespace($usedClass) == $className) {
$returnType = $usedClass;
break;
}
}
if ($isReferenceToArrayOfObjects) {
$returnType .= '[]';
}
}
return Utils::sanitizeDeclaration(
$returnType,
$method->getDeclaringClass()->getNamespaceName()
);
}
/**
* @param string $classRef
* @return bool
*/
private function classExists($classRef)
{
return class_exists(trim($classRef, '[]'));
}
/**
* @param string $className
* @return string
*/
private function stripAwayNamespace($className)
{
return trim(substr($className, strrpos($className, '\\')), '\\');
}
/**
* @param DocInfo $info
* @param ReflectionMethod $method
* @param ClassEntity $class
* @return bool
*/
protected function shouldIgnoreFunction($info, ReflectionMethod $method, $class)
{
return $info->shouldBeIgnored() ||
$method->isPrivate() ||
!$class->isSame($method->getDeclaringClass()->getName());
}
/**
* @todo Turn this into a class "FunctionEntityFactory"
* @param \ReflectionParameter $reflection
* @param array $docs
* @return FunctionEntity
*/
private function createParameterEntity(\ReflectionParameter $reflection, $docs)
{
// need to use slash instead of pipe or md-generation will get it wrong
$def = false;
$type = 'mixed';
$declaredType = self::getParamType($reflection);
if( !isset($docs['type']) )
$docs['type'] = '';
if( $declaredType && !($declaredType=='array' && substr($docs['type'], -2) == '[]') && $declaredType != $docs['type']) {
if( $declaredType && $docs['type'] ) {
$posClassA = Utils::getClassBaseName($docs['type']);
$posClassB = Utils::getClassBaseName($declaredType);
if( $posClassA == $posClassB ) {
$docs['type'] = $declaredType;
} else {
$docs['type'] = empty($docs['type']) ? $declaredType : $docs['type'].'/'.$declaredType;
}
} else {
$docs['type'] = empty($docs['type']) ? $declaredType : $docs['type'].'/'.$declaredType;
}
}
try {
$def = $reflection->getDefaultValue();
$type = $this->getTypeFromVal($def);
if( is_string($def) ) {
$def = "`'$def'`";
} elseif( is_bool($def) ) {
$def = $def ? 'true':'false';
} elseif( is_null($def) ) {
$def = 'null';
} elseif( is_array($def) ) {
$def = 'array()';
}
} catch(\Exception $e) {}
$varName = '$'.$reflection->getName();
if( !empty($docs) ) {
$docs['default'] = $def;
if( $type == 'mixed' && $def == 'null' && strpos($docs['type'], '\\') === 0 ) {
$type = false;
}
if( $type && $def && !empty($docs['type']) && $docs['type'] != $type && strpos($docs['type'], '|') === false) {
if( substr($docs['type'], strpos($docs['type'], '\\')) == substr($declaredType, strpos($declaredType, '\\')) ) {
$docs['type'] = $declaredType;
} else {
$docs['type'] = ($type == 'mixed' ? '':$type.'/').$docs['type'];
}
} elseif( $type && empty($docs['type']) ) {
$docs['type'] = $type;
}
} else {
$docs = [
'descriptions'=>'',
'name' => $varName,
'default' => $def,
'type' => $type
];
}
$param = new ParamEntity();
$param->setDescription(isset($docs['description']) ? $docs['description']:'');
$param->setName($varName);
$param->setDefault($docs['default']);
$param->setType(empty($docs['type']) ? 'mixed':str_replace(['|', '\\\\'], ['/', '\\'], $docs['type']));
return $param;
}
/**
* Tries to find out if the type of the given parameter. Will
* return empty string if not possible.
*
* @example
* <code>
* <?php
* $reflector = new \\ReflectionClass('MyClass');
* foreach($reflector->getMethods() as $method ) {
* foreach($method->getParameters() as $param) {
* $name = $param->getName();
* $type = Reflector::getParamType($param);
* printf("%s = %s\n", $name, $type);
* }
* }
* </code>
*
* @param \ReflectionParameter $refParam
* @return string
*/
static function getParamType(\ReflectionParameter $refParam)
{
$export = \ReflectionParameter::export([
$refParam->getDeclaringClass()->name,
$refParam->getDeclaringFunction()->name
],
$refParam->name,
true
);
$export = str_replace(' or NULL', '', $export);
$type = preg_replace('/.*?([\w\\\]+)\s+\$'.current(explode('=', $refParam->name)).'.*/', '\\1', $export);
if( strpos($type, 'Parameter ') !== false ) {
return '';
}
if( $type != 'array' && strpos($type, '\\') !== 0 ) {
$type = '\\'.$type;
}
return $type;
}
/**
* @param string $name
* @return string
*/
private function guessReturnTypeFromFuncName($name)
{
$mixed = ['get', 'load', 'fetch', 'find', 'create'];
$bool = ['is', 'can', 'has', 'have', 'should'];
foreach($mixed as $prefix) {
if( strpos($name, $prefix) === 0 )
return 'mixed';
}
foreach($bool as $prefix) {
if( strpos($name, $prefix) === 0 )
return 'bool';
}
return 'void';
}
/**
* @param string $def
* @return string
*/
private function getTypeFromVal($def)
{
if( is_string($def) ) {
return 'string';
} elseif( is_bool($def) ) {
return 'bool';
} elseif( is_array($def) ) {
return 'array';
} else {
return 'mixed';
}
}
/**
* @param FunctionEntity $func
* @param ClassEntity $class
* @return FunctionEntity
*/
private function findInheritedFunctionDeclaration(FunctionEntity $func, ClassEntity $class)
{
$funcName = $func->getName();
$inheritedFuncDeclaration = $this->functionFinder->find(
$funcName,
$class->getExtends()
);
if (!$inheritedFuncDeclaration) {
$inheritedFuncDeclaration = $this->functionFinder->findInClasses(
$funcName,
$class->getInterfaces()
);
if (!$inheritedFuncDeclaration) {
throw new \RuntimeException(
'Function '.$funcName.' tries to inherit docs but no parent method is found'
);
}
}
if (!$func->isAbstract() && !$class->isAbstract() && $inheritedFuncDeclaration->isAbstract()) {
$inheritedFuncDeclaration->isAbstract(false);
}
return $inheritedFuncDeclaration;
}
/**
* @param ReflectionMethod $method
* @param DocInfo $docInfo
* @return array
*/
private function getParams(ReflectionMethod $method, $docInfo)
{
$params = [];
foreach ($method->getParameters() as $param) {
$paramName = '$' . $param->getName();
$params[$param->getName()] = $this->createParameterEntity(
$param,
$docInfo->getParameterInfo($paramName)
);
}
return array_values($params);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace PHPDocsMD;
/**
* Interface for classes that can compute ClassEntity objects
* @package PHPDocsMD
*/
interface ReflectorInterface
{
/**
* @return \PHPDocsMD\ClassEntity
*/
function getClassEntity();
}

View File

@@ -0,0 +1,50 @@
<?php
namespace PHPDocsMD;
/**
* Class that can extract all use statements in a file
* @package PHPDocsMD
*/
class UseInspector
{
/**
* @param string $content
* @return string[]
*/
public function getUseStatementsInString($content)
{
$usages = [];
$chunks = array_slice(preg_split('/use[\s+]/', $content), 1);
foreach ($chunks as $chunk) {
$usage = trim(current(explode(';', $chunk)));
$usages[] = Utils::sanitizeClassName($usage);
}
return $usages;
}
/**
* @param string $filePath
* @return array
*/
public function getUseStatementsInFile($filePath)
{
return $this->getUseStatementsInString(file_get_contents($filePath));
}
/**
* @param \ReflectionClass $reflectionClass
* @return array
*/
public function getUseStatements(\ReflectionClass $reflectionClass)
{
$classUseStatements = [];
$classFile = $reflectionClass->getFileName();
if ($classFile) {
$classUseStatements = $this->getUseStatementsInFile($classFile);
}
return $classUseStatements;
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace PHPDocsMD;
/**
* @package PHPDocsMD
*/
class Utils
{
/**
* @param string $name
* @return string
*/
public static function sanitizeClassName($name)
{
return '\\'.trim($name, ' \\');
}
/**
* @param string $fullClassName
* @return string
*/
public static function getClassBaseName($fullClassName)
{
$parts = explode('\\', trim($fullClassName));
return end($parts);
}
/**
* @param string $typeDeclaration
* @param string $currentNameSpace
* @param string $delimiter
* @return string
*/
public static function sanitizeDeclaration($typeDeclaration, $currentNameSpace, $delimiter='|')
{
$parts = explode($delimiter, $typeDeclaration);
foreach($parts as $i=>$p) {
if (self::shouldPrefixWithNamespace($p)) {
$p = self::sanitizeClassName('\\' . trim($currentNameSpace, '\\') . '\\' . $p);
} elseif (self::isClassReference($p)) {
$p = self::sanitizeClassName($p);
}
$parts[$i] = $p;
}
return implode('/', $parts);
}
/**
* @param string $typeDeclaration
* @return bool
*/
private static function shouldPrefixWithNameSpace($typeDeclaration)
{
return strpos($typeDeclaration, '\\') !== 0 && self::isClassReference($typeDeclaration);
}
/**
* @param string $typeDeclaration
* @return bool
*/
public static function isClassReference($typeDeclaration)
{
$natives = [
'mixed',
'string',
'int',
'float',
'integer',
'number',
'bool',
'boolean',
'object',
'false',
'true',
'null',
'array',
'void',
'callable'
];
$sanitizedTypeDeclaration = rtrim(trim(strtolower($typeDeclaration)), '[]');
return !in_array($sanitizedTypeDeclaration, $natives) &&
strpos($typeDeclaration, ' ') === false;
}
public static function isNativeClassReference($typeDeclaration)
{
$sanitizedType = str_replace('[]', '', $typeDeclaration);
if (Utils::isClassReference($typeDeclaration) && class_exists($sanitizedType, false)) {
$reflectionClass = new \ReflectionClass($sanitizedType);
return !$reflectionClass->getFileName();
}
return false;
}
}