作者 Karson

新增自定义插件API文档生成

新增自定义插件API文档生成
新增登录和鉴权状态显示
新增自定义测试提交参数
... ... @@ -23,9 +23,10 @@ class Api extends Command
->addOption('template', 'e', Option::VALUE_OPTIONAL, '', 'index.html')
->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force override general file', false)
->addOption('title', 't', Option::VALUE_OPTIONAL, 'document title', $site['name'])
->addOption('author', 'a', Option::VALUE_OPTIONAL, 'document author', $site['name'])
->addOption('class', 'c', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, 'extend class', null)
->addOption('language', 'l', Option::VALUE_OPTIONAL, 'language', 'zh-cn')
->addOption('addon', 'a', Option::VALUE_OPTIONAL, 'addon name', null)
->addOption('controller', 'r', Option::VALUE_REQUIRED | Option::VALUE_IS_ARRAY, 'controller name', null)
->setDescription('Build Api document from controller');
}
... ... @@ -58,12 +59,21 @@ class Api extends Command
$classes = $input->getOption('class');
// 标题
$title = $input->getOption('title');
// 作者
$author = $input->getOption('author');
// 模块
$module = $input->getOption('module');
$moduleDir = APP_PATH . $module . DS;
// 插件
$addon = $input->getOption('addon');
$moduleDir = $addonDir = '';
if ($addon) {
$addonInfo = get_addon_info($addon);
if (!$addonInfo) {
throw new Exception('addon not found');
}
$moduleDir = ADDON_PATH . $addon . DS;
} else {
$moduleDir = APP_PATH . $module . DS;
}
if (!is_dir($moduleDir)) {
throw new Exception('module not found');
}
... ... @@ -81,31 +91,44 @@ class Api extends Command
}
}
$controllerDir = $moduleDir . Config::get('url_controller_layer') . DS;
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($controllerDir),
\RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($files as $name => $file) {
if (!$file->isDir() && $file->getExtension() == 'php') {
$filePath = $file->getRealPath();
//控制器名
$controller = $input->getOption('controller') ?: [];
if (!$controller) {
$controllerDir = $moduleDir . Config::get('url_controller_layer') . DS;
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($controllerDir),
\RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($files as $name => $file) {
if (!$file->isDir() && $file->getExtension() == 'php') {
$filePath = $file->getRealPath();
$classes[] = $this->get_class_from_file($filePath);
}
}
} else {
foreach ($controller as $index => $item) {
$filePath = $moduleDir . Config::get('url_controller_layer') . DS . $item . '.php';
$classes[] = $this->get_class_from_file($filePath);
}
}
$classes = array_unique(array_filter($classes));
$config = [
'sitename' => config('site.name'),
'title' => $title,
'author' => $author,
'author' => config('site.name'),
'description' => '',
'apiurl' => $url,
'language' => $language,
];
$builder = new Builder($classes);
$content = $builder->render($template_file, ['config' => $config, 'lang' => $lang]);
try {
$builder = new Builder($classes);
$content = $builder->render($template_file, ['config' => $config, 'lang' => $lang]);
} catch (\Exception $e) {
print_r($e);
}
if (!file_put_contents($output_file, $content)) {
throw new Exception('Cannot save the content to ' . $output_file);
}
... ...
... ... @@ -16,6 +16,9 @@ return [
'Tokentips' => 'Token在会员注册或登录后都会返回,WEB端同时存在于Cookie中',
'Apiurltips' => 'API接口URL',
'Savetips' => '点击保存后Token和Api url都将保存在本地Localstorage中',
'Authorization' => '权限',
'NeedLogin' => '登录',
'NeedRight' => '鉴权',
'ReturnHeaders' => '响应头',
'ReturnParameters' => '返回参数',
'Response' => '响应输出',
... ...
... ... @@ -43,9 +43,11 @@ class Builder
continue;
}
Extractor::getClassMethodAnnotations($class);
//Extractor::getClassPropertyValues($class);
}
$allClassAnnotation = Extractor::getAllClassAnnotations();
$allClassMethodAnnotation = Extractor::getAllClassMethodAnnotations();
//$allClassPropertyValue = Extractor::getAllClassPropertyValues();
// foreach ($allClassMethodAnnotation as $className => &$methods) {
// foreach ($methods as &$method) {
... ... @@ -162,10 +164,12 @@ class Builder
list($allClassAnnotations, $allClassMethodAnnotations) = $this->extractAnnotations();
$sectorArr = [];
foreach ($allClassAnnotations as $index => $allClassAnnotation) {
foreach ($allClassAnnotations as $index => &$allClassAnnotation) {
$sector = isset($allClassAnnotation['ApiSector']) ? $allClassAnnotation['ApiSector'][0] : $allClassAnnotation['ApiTitle'][0];
$sectorArr[$sector] = isset($allClassAnnotation['ApiWeigh']) ? $allClassAnnotation['ApiWeigh'][0] : 0;
}
unset($allClassAnnotation);
arsort($sectorArr);
$routes = include_once CONF_PATH . 'route.php';
$subdomain = false;
... ... @@ -175,7 +179,7 @@ class Builder
$counter = 0;
$section = null;
$weigh = 0;
$docslist = [];
$docsList = [];
foreach ($allClassMethodAnnotations as $class => $methods) {
foreach ($methods as $name => $docs) {
if (isset($docs['ApiSector'][0])) {
... ... @@ -190,28 +194,30 @@ class Builder
if ($subdomain) {
$route = substr($route, 4);
}
$docslist[$section][$class . $name] = [
'id' => $counter,
'method' => is_array($docs['ApiMethod'][0]) ? $docs['ApiMethod'][0]['data'] : $docs['ApiMethod'][0],
'method_label' => $this->generateBadgeForMethod($docs),
'section' => $section,
'route' => $route,
'title' => is_array($docs['ApiTitle'][0]) ? $docs['ApiTitle'][0]['data'] : $docs['ApiTitle'][0],
'summary' => is_array($docs['ApiSummary'][0]) ? $docs['ApiSummary'][0]['data'] : $docs['ApiSummary'][0],
'body' => isset($docs['ApiBody'][0]) ? is_array($docs['ApiBody'][0]) ? $docs['ApiBody'][0]['data'] : $docs['ApiBody'][0] : '',
'headerslist' => $this->generateHeadersTemplate($docs),
'paramslist' => $this->generateParamsTemplate($docs),
'returnheaderslist' => $this->generateReturnHeadersTemplate($docs),
'returnparamslist' => $this->generateReturnParamsTemplate($docs),
'weigh' => is_array($docs['ApiWeigh'][0]) ? $docs['ApiWeigh'][0]['data'] : $docs['ApiWeigh'][0],
'return' => isset($docs['ApiReturn']) ? is_array($docs['ApiReturn'][0]) ? $docs['ApiReturn'][0]['data'] : $docs['ApiReturn'][0] : '',
$docsList[$section][$name] = [
'id' => $counter,
'method' => is_array($docs['ApiMethod'][0]) ? $docs['ApiMethod'][0]['data'] : $docs['ApiMethod'][0],
'methodLabel' => $this->generateBadgeForMethod($docs),
'section' => $section,
'route' => $route,
'title' => is_array($docs['ApiTitle'][0]) ? $docs['ApiTitle'][0]['data'] : $docs['ApiTitle'][0],
'summary' => is_array($docs['ApiSummary'][0]) ? $docs['ApiSummary'][0]['data'] : $docs['ApiSummary'][0],
'body' => isset($docs['ApiBody'][0]) ? is_array($docs['ApiBody'][0]) ? $docs['ApiBody'][0]['data'] : $docs['ApiBody'][0] : '',
'headersList' => $this->generateHeadersTemplate($docs),
'paramsList' => $this->generateParamsTemplate($docs),
'returnHeadersList' => $this->generateReturnHeadersTemplate($docs),
'returnParamsList' => $this->generateReturnParamsTemplate($docs),
'weigh' => is_array($docs['ApiWeigh'][0]) ? $docs['ApiWeigh'][0]['data'] : $docs['ApiWeigh'][0],
'return' => isset($docs['ApiReturn']) ? is_array($docs['ApiReturn'][0]) ? $docs['ApiReturn'][0]['data'] : $docs['ApiReturn'][0] : '',
'needLogin' => $docs['ApiPermissionLogin'][0],
'needRight' => $docs['ApiPermissionRight'][0],
];
$counter++;
}
}
//重建排序
foreach ($docslist as $index => &$methods) {
foreach ($docsList as $index => &$methods) {
$methodSectorArr = [];
foreach ($methods as $name => $method) {
$methodSectorArr[$name] = isset($method['weigh']) ? $method['weigh'] : 0;
... ... @@ -219,9 +225,8 @@ class Builder
arsort($methodSectorArr);
$methods = array_merge(array_flip(array_keys($methodSectorArr)), $methods);
}
$docslist = array_merge(array_flip(array_keys($sectorArr)), $docslist);
$docslist = array_filter($docslist , function($v) {return is_array($v) ; }) ;
return $docslist;
$docsList = array_merge(array_flip(array_keys($sectorArr)), $docsList);
return $docsList;
}
public function getView()
... ... @@ -237,8 +242,8 @@ class Builder
*/
public function render($template, $vars = [])
{
$docslist = $this->parse();
$docsList = $this->parse();
return $this->view->display(file_get_contents($template), array_merge($vars, ['docslist' => $docslist]));
return $this->view->display(file_get_contents($template), array_merge($vars, ['docsList' => $docsList]));
}
}
... ...
... ... @@ -24,6 +24,8 @@ class Extractor
private static $classMethodAnnotationCache;
private static $classPropertyValueCache;
/**
* Indicates that annotations should has strict behavior, 'false' by default
* @var boolean
... ... @@ -66,14 +68,16 @@ class Extractor
/**
* Gets all anotations with pattern @SomeAnnotation() from a given class
*
* @param string $className class name to get annotations
* @param string $className class name to get annotations
* @return array self::$classAnnotationCache all annotated elements
*/
public static function getClassAnnotations($className)
{
if (!isset(self::$classAnnotationCache[$className])) {
$class = new \ReflectionClass($className);
self::$classAnnotationCache[$className] = self::parseAnnotations($class->getDocComment());
$annotationArr = self::parseAnnotations($class->getDocComment());
$annotationArr['ApiTitle'] = !isset($annotationArr['ApiTitle'][0]) || !trim($annotationArr['ApiTitle'][0]) ? [$class->getShortName()] : $annotationArr['ApiTitle'];
self::$classAnnotationCache[$className] = $annotationArr;
}
return self::$classAnnotationCache[$className];
... ... @@ -96,6 +100,17 @@ class Extractor
return self::$classMethodAnnotationCache[$className];
}
public static function getClassPropertyValues($className)
{
$class = new \ReflectionClass($className);
foreach ($class->getProperties() as $object) {
self::$classPropertyValueCache[$className][$object->name] = self::getClassPropertyValue($className, $object->name);
}
return self::$classMethodAnnotationCache[$className];
}
public static function getAllClassAnnotations()
{
return self::$classAnnotationCache;
... ... @@ -106,11 +121,25 @@ class Extractor
return self::$classMethodAnnotationCache;
}
public static function getAllClassPropertyValues()
{
return self::$classPropertyValueCache;
}
public static function getClassPropertyValue($className, $property)
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$reflectionClass = new \ReflectionClass($className);
$reflectionProperty = $reflectionClass->getProperty($property);
$reflectionProperty->setAccessible(true);
return $reflectionProperty->getValue($reflectionClass->newInstanceWithoutConstructor());
}
/**
* Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class
*
* @param string $className class name
* @param string $methodName method name to get annotations
* @param string $className class name
* @param string $methodName method name to get annotations
* @return array self::$annotationCache all annotated elements of a method given
*/
public static function getMethodAnnotations($className, $methodName)
... ... @@ -138,8 +167,8 @@ class Extractor
* Gets all anotations with pattern @SomeAnnotation() from a determinated method of a given class
* and instance its abcAnnotation class
*
* @param string $className class name
* @param string $methodName method name to get annotations
* @param string $className class name
* @param string $methodName method name to get annotations
* @return array self::$annotationCache all annotated objects of a method given
*/
public function getMethodAnnotationsObjects($className, $methodName)
... ... @@ -189,7 +218,11 @@ class Extractor
$methodName = $method->getName();
$methodAnnotations = self::parseAnnotations($docblockMethod);
$methodAnnotations['ApiTitle'] = !isset($methodAnnotations['ApiTitle'][0]) || !trim($methodAnnotations['ApiTitle'][0]) ? [$method->getName()] : $methodAnnotations['ApiTitle'];
$classAnnotations = self::parseAnnotations($dockblockClass);
$classAnnotations['ApiTitle'] = !isset($classAnnotations['ApiTitle'][0]) || !trim($classAnnotations['ApiTitle'][0]) ? [$class->getShortName()] : $classAnnotations['ApiTitle'];
if (isset($methodAnnotations['ApiInternal']) || $methodName == '_initialize' || $methodName == '_empty') {
return [];
}
... ... @@ -264,15 +297,15 @@ class Extractor
}
}
$methodAnnotations['ApiPermissionLogin'] = [!in_array('*', $noNeedLogin) && !in_array($methodName, $noNeedLogin)];
$methodAnnotations['ApiPermissionRight'] = [!in_array('*', $noNeedRight) && !in_array($methodName, $noNeedRight)];
$methodAnnotations['ApiPermissionRight'] = !$methodAnnotations['ApiPermissionLogin'][0] ? false : [!in_array('*', $noNeedRight) && !in_array($methodName, $noNeedRight)];
return $methodAnnotations;
}
/**
* Parse annotations
*
* @param string $docblock
* @param string $name
* @param string $docblock
* @param string $name
* @return array parsed annotations params
*/
private static function parseCustomAnnotations($docblock, $name = 'param')
... ... @@ -291,7 +324,7 @@ class Extractor
/**
* Parse annotations
*
* @param string $docblock
* @param string $docblock
* @return array parsed annotations params
*/
private static function parseAnnotations($docblock)
... ... @@ -337,7 +370,7 @@ class Extractor
/**
* Parse individual annotation arguments
*
* @param string $content arguments string
* @param string $content arguments string
* @return array annotated arguments
*/
private static function parseArgs($content)
... ... @@ -480,8 +513,8 @@ class Extractor
/**
* Try determinate the original type variable of a string
*
* @param string $val string containing possibles variables that can be cast to bool or int
* @param boolean $trim indicate if the value passed should be trimmed after to try cast
* @param string $val string containing possibles variables that can be cast to bool or int
* @param boolean $trim indicate if the value passed should be trimmed after to try cast
* @return mixed returns the value converted to original type if was possible
*/
private static function castValue($val, $trim = false)
... ...
... ... @@ -27,12 +27,12 @@
font-family: "Roboto", "SF Pro SC", "SF Pro Display", "SF Pro Icons", "PingFang SC", BlinkMacSystemFont, -apple-system, "Segoe UI", "Microsoft Yahei", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
font-weight: 400;
}
h2 { font-size: 1.6em; }
h2 { font-size: 1.2em; }
hr { margin-top: 10px; }
.tab-pane { padding-top: 10px; }
.mt0 { margin-top: 0px; }
.footer { font-size: 12px; color: #666; }
.label { display: inline-block; min-width: 65px; padding: 0.3em 0.6em 0.3em; }
.docs-list .label { display: inline-block; min-width: 65px; padding: 0.3em 0.6em 0.3em; }
.string { color: green; }
.number { color: darkorange; }
.boolean { color: blue; }
... ... @@ -65,12 +65,24 @@
#sidebar > .list-group > a{
text-indent:0;
}
#sidebar .child > a .tag{
position: absolute;
right: 10px;
top: 11px;
}
#sidebar .child > a .pull-right{
margin-left:3px;
}
#sidebar .child {
border:1px solid #ddd;
border-bottom:none;
}
#sidebar .child:last-child {
border-bottom:1px solid #ddd;
}
#sidebar .child > a {
border:0;
min-height: 40px;
}
#sidebar .list-group a.current {
background:#f5f5f5;
... ... @@ -94,6 +106,9 @@
.label-primary {
background-color: #248aff;
}
.docs-list .panel .panel-body .table {
margin-bottom: 0;
}
</style>
</head>
... ... @@ -138,25 +153,34 @@
<!-- menu -->
<div id="sidebar">
<div class="list-group panel">
{foreach name="docslist" id="docs"}
{foreach name="docsList" id="docs"}
<a href="#{$key}" class="list-group-item" data-toggle="collapse" data-parent="#sidebar">{$key} <i class="fa fa-caret-down"></i></a>
<div class="child collapse" id="{$key}">
{foreach name="docs" id="api" }
<a href="javascript:;" data-id="{$api.id}" class="list-group-item">{$api.title}</a>
<a href="javascript:;" data-id="{$api.id}" class="list-group-item">{$api.title}
<span class="tag">
{if $api.needRight}
<span class="label label-danger pull-right"></span>
{/if}
{if $api.needLogin}
<span class="label label-success pull-right noneedlogin"></span>
{/if}
</span>
</a>
{/foreach}
</div>
{/foreach}
</div>
</div>
<div class="panel-group" id="accordion">
{foreach name="docslist" id="docs"}
<div class="panel-group docs-list" id="accordion">
{foreach name="docsList" id="docs"}
<h2>{$key}</h2>
<hr>
{foreach name="docs" id="api" }
<div class="panel panel-default">
<div class="panel-heading" id="heading-{$api.id}">
<h4 class="panel-title">
<span class="label {$api.method_label}">{$api.method|strtoupper}</span>
<span class="label {$api.methodLabel}">{$api.method|strtoupper}</span>
<a data-toggle="collapse" data-parent="#accordion{$api.id}" href="#collapseOne{$api.id}"> {$api.title} <span class="text-muted">{$api.route}</span></a>
</h4>
</div>
... ... @@ -178,9 +202,26 @@
{$api.summary}
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>{$lang.Authorization}</strong></div>
<div class="panel-body">
<table class="table table-hover">
<tbody>
<tr>
<td>{$lang.NeedLogin}</td>
<td>{$api.needLogin?'是':'否'}</td>
</tr>
<tr>
<td>{$lang.NeedRight}</td>
<td>{$api.needRight?'是':'否'}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading"><strong>{$lang.Headers}</strong></div>
<div class="panel-body">
{if $api.headerslist}
{if $api.headersList}
<table class="table table-hover">
<thead>
<tr>
... ... @@ -191,7 +232,7 @@
</tr>
</thead>
<tbody>
{foreach name="api['headerslist']" id="header"}
{foreach name="api['headersList']" id="header"}
<tr>
<td>{$header.name}</td>
<td>{$header.type}</td>
... ... @@ -209,7 +250,7 @@
<div class="panel panel-default">
<div class="panel-heading"><strong>{$lang.Parameters}</strong></div>
<div class="panel-body">
{if $api.paramslist}
{if $api.paramsList}
<table class="table table-hover">
<thead>
<tr>
... ... @@ -220,7 +261,7 @@
</tr>
</thead>
<tbody>
{foreach name="api['paramslist']" id="param"}
{foreach name="api['paramsList']" id="param"}
<tr>
<td>{$param.name}</td>
<td>{$param.type}</td>
... ... @@ -246,12 +287,12 @@
<div class="tab-pane" id="sandbox{$api.id}">
<div class="row">
<div class="col-md-12">
{if $api.headerslist}
{if $api.headersList}
<div class="panel panel-default">
<div class="panel-heading"><strong>{$lang.Headers}</strong></div>
<div class="panel-body">
<div class="headers">
{foreach name="api['headerslist']" id="param"}
{foreach name="api['headersList']" id="param"}
<div class="form-group">
<label class="control-label" for="{$param.name}">{$param.name}</label>
<input type="{$param.type}" class="form-control input-sm" id="{$param.name}" {if $param.required}required{/if} placeholder="{$param.description} - Ex: {$param.sample}" name="{$param.name}">
... ... @@ -262,11 +303,15 @@
</div>
{/if}
<div class="panel panel-default">
<div class="panel-heading"><strong>{$lang.Parameters}</strong></div>
<div class="panel-heading"><strong>{$lang.Parameters}</strong>
<div class="pull-right">
<a href="javascript:" class="btn btn-xs btn-info btn-append">追加</a>
</div>
</div>
<div class="panel-body">
<form enctype="application/x-www-form-urlencoded" role="form" action="{$api.route}" method="{$api.method}" name="form{$api.id}" id="form{$api.id}">
{if $api.paramslist}
{foreach name="api['paramslist']" id="param"}
{if $api.paramsList}
{foreach name="api['paramsList']" id="param"}
<div class="form-group">
<label class="control-label" for="{$param.name}">{$param.name}</label>
<input type="{$param.type}" class="form-control input-sm" id="{$param.name}" {if $param.required}required{/if} placeholder="{$param.description}{if $param.sample} - 例: {$param.sample}{/if}" name="{$param.name}">
... ... @@ -277,7 +322,7 @@
</div>
{/if}
<div class="form-group">
<div class="form-group form-group-submit">
<button type="submit" class="btn btn-success send" rel="{$api.id}">{$lang.Send}</button>
<button type="reset" class="btn btn-info" rel="{$api.id}">{$lang.Reset}</button>
</div>
... ... @@ -298,7 +343,7 @@
<div class="panel panel-default">
<div class="panel-heading"><strong>{$lang.ReturnParameters}</strong></div>
<div class="panel-body">
{if $api.returnparamslist}
{if $api.returnParamsList}
<table class="table table-hover">
<thead>
<tr>
... ... @@ -308,7 +353,7 @@
</tr>
</thead>
<tbody>
{foreach name="api['returnparamslist']" id="param"}
{foreach name="api['returnParamsList']" id="param"}
<tr>
<td>{$param.name}</td>
<td>{$param.type}</td>
... ... @@ -346,10 +391,10 @@
<div class="row mt0 footer">
<div class="col-md-6" align="left">
Generated on {:date('Y-m-d H:i:s')}
</div>
<div class="col-md-6" align="right">
<a href="./" target="_blank">{$config.sitename}</a>
Generated on {:date('Y-m-d H:i:s')} <a href="./" target="_blank">{$config.sitename}</a>
</div>
</div>
... ... @@ -475,7 +520,7 @@
$sample.html('<pre>' + str + '</pre>');
});
$('body').on('click', '#save_data', function (e) {
$(document).on('click', '#save_data', function (e) {
if (storage) {
storage.setItem('token', $('#token').val());
storage.setItem('apiUrl', $('#apiUrl').val());
... ... @@ -483,8 +528,20 @@
alert('Your browser does not support local storage');
}
});
$(document).on('click', '.btn-append', function (e) {
$($("#appendtpl").html()).insertBefore($(this).closest(".panel").find(".form-group-submit"));
return false;
});
$(document).on('click', '.btn-remove', function (e) {
$(this).closest(".form-group").remove();
return false;
});
$(document).on('keyup', '.input-custom-name', function (e) {
$(this).closest(".row").find(".input-custom-value").attr("name", $(this).val());
return false;
});
$('body').on('click', '.send', function (e) {
$(document).on('click', '.send', function (e) {
e.preventDefault();
var form = $(this).closest('form');
//added /g to get all the matched params instead of only first
... ... @@ -577,5 +634,21 @@
});
});
</script>
<script type="text/html" id="appendtpl">
<div class="form-group">
<label class="control-label">自定义</label>
<div class="row">
<div class="col-xs-4">
<input type="text" class="form-control input-sm input-custom-name" placeholder="名称">
</div>
<div class="col-xs-6">
<input type="text" class="form-control input-sm input-custom-value" placeholder="值">
</div>
<div class="col-xs-2 text-center">
<a href="javascript:" class="btn btn-sm btn-danger btn-remove">删除</a>
</div>
</div>
</div>
</script>
</body>
</html>
... ...
此 diff 太大无法显示。