You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
304 lines
9.5 KiB
304 lines
9.5 KiB
<?php |
|
|
|
namespace Plugins\ApiDoc\Admin\Service; |
|
|
|
use App\Api\Middleware\ApiAuthenticateMiddleware; |
|
use App\Api\Middleware\ApiTokenSetMiddleware; |
|
use App\Common\Util\TP; |
|
use Plugins\ApiDoc\Admin\Attributes\Api; |
|
use Plugins\ApiDoc\Admin\Attributes\ApiBody; |
|
use Plugins\ApiDoc\Admin\Attributes\ApiQuery; |
|
use Plugins\ApiDoc\Admin\Attributes\ApiReturn; |
|
use Plugins\ApiDoc\Admin\Attributes\ApiVersion; |
|
use Sc\Util\ImitateAopProxy\AopProxyTrait; |
|
use Sc\Util\Tool; |
|
|
|
/** |
|
* Class ApiDocService |
|
*/ |
|
class ApiDocService |
|
{ |
|
use AopProxyTrait; |
|
|
|
private array $config = []; |
|
private array $allRouteMap = []; |
|
|
|
/** |
|
* @throws \Exception |
|
*/ |
|
public function get(): array |
|
{ |
|
$appName = TP::config('app.app_name', ''); |
|
$getPath = $this->getDefaultConfig('get_path'); |
|
$this->allRouteMap = array_column(array_filter(TP::route()->getRouteList(), fn($route) => is_string($route['route'])), null, 'route'); |
|
|
|
$apiLists = $this->scan($getPath); |
|
|
|
return [ |
|
"title" => $appName . 'API 接口文档', |
|
"version" => array_unique(array_merge(...array_column($apiLists, 'version'))), |
|
'host' => Tool::url(TP::request()->url(true))->getDomain(), |
|
'apiLists' => $apiLists, |
|
]; |
|
} |
|
|
|
/** |
|
* @param string|null $key |
|
* |
|
* @return array|mixed |
|
*/ |
|
private function getDefaultConfig(string $key = null): mixed |
|
{ |
|
if (!$this->config) { |
|
$this->config = TP::config('plugins.ApiDoc') ?: include __DIR__ . '/../../config.php'; |
|
} |
|
|
|
return $key === null ? $this->config : $this->config[$key]; |
|
} |
|
|
|
/** |
|
* @param array $paths |
|
* |
|
* @return array |
|
* @throws \Exception |
|
*/ |
|
private function scan(array $paths): array |
|
{ |
|
$apiList = []; |
|
|
|
foreach ($paths as ['path' => $path, 'namespace' => $namespace, 'namespace_root' => $namespaceRoot, ]) { |
|
$MPaths = $this->getPaths($path); |
|
|
|
foreach ($MPaths as $MPath) { |
|
try { |
|
Tool::dir($MPath)->each(function (Tool\Dir\EachFile $eachFile) use ($path, $namespace, $namespaceRoot, &$apiList){ |
|
$classname = strtr(basename($eachFile->filename), ['.php' => '']); |
|
if (!str_ends_with($eachFile->filename, '.php') || !preg_match('/^[a-zA-Z1-9]+$/', $classname)){ |
|
return; |
|
} |
|
$classFullName = implode("\\", [$namespace, ...explode(DIRECTORY_SEPARATOR, strtr(dirname($eachFile->filepath), [$namespaceRoot => ''])), $classname]); |
|
$classFullName = preg_replace('/\\\+/', '\\', $classFullName); |
|
try { |
|
if (!class_exists($classFullName)) { |
|
return; |
|
} |
|
$reflectionClass = $this->apiClassCheck($classFullName); |
|
if ($reflectionClass) { |
|
$apiList[] = $this->apiResolve($reflectionClass); |
|
} |
|
} catch (\Throwable $throwable) {} |
|
}); |
|
} catch (\Exception $exception) {} |
|
} |
|
} |
|
|
|
return $apiList; |
|
} |
|
|
|
/** |
|
* @param $class |
|
* |
|
* @return \ReflectionClass|null |
|
* @throws \ReflectionException |
|
*/ |
|
private function apiClassCheck($class): ?\ReflectionClass |
|
{ |
|
$reflexClass = new \ReflectionClass($class); |
|
|
|
if (!$reflexClass->getAttributes(Api::class)) { |
|
return null; |
|
} |
|
|
|
return $reflexClass; |
|
} |
|
|
|
/** |
|
* @param \ReflectionClass $reflectionClass |
|
* |
|
* @return array |
|
*/ |
|
private function apiResolve(\ReflectionClass $reflectionClass): array |
|
{ |
|
$groupTitle = $this->apiTitleResolve($reflectionClass->getDocComment()); |
|
$apis = []; |
|
|
|
preg_match('#\\\(\w+)\\\Controller#', $reflectionClass->getNamespaceName(), $match); |
|
if ($match) { |
|
$baseUri = array_search($match[1], TP::config('app.app_map')); |
|
foreach ($reflectionClass->getMethods() as $reflectionMethod) { |
|
if ($api = $this->methodResolve($reflectionMethod)) { |
|
$api['url'] = "/" . $baseUri . '/' . $api['url']; |
|
$apis[] = $api; |
|
} |
|
} |
|
} |
|
|
|
return [ |
|
'name' => $groupTitle, |
|
'version' => array_unique(array_merge(...array_column($apis, 'version'))), |
|
'children' => $apis, |
|
]; |
|
} |
|
|
|
/** |
|
* @param string $comment |
|
* |
|
* @return string |
|
*/ |
|
private function apiTitleResolve(string $comment): string |
|
{ |
|
preg_match('/^\/(\*|\s)+([^\*\s]+)/', $comment, $document); |
|
|
|
return empty($document[2]) ? "未命名" : $document[2]; |
|
} |
|
|
|
/** |
|
* @param array $route |
|
* |
|
* @return string|null |
|
*/ |
|
private function authenticateType(array $route): ?string |
|
{ |
|
$haveMiddlewares = $route['option']['middleware'] ?? []; |
|
|
|
if (in_array(ApiAuthenticateMiddleware::class, $haveMiddlewares)) { |
|
return "strength"; |
|
} |
|
|
|
if (in_array(ApiTokenSetMiddleware::class, $haveMiddlewares)) { |
|
return "weak"; |
|
} |
|
|
|
return null; |
|
} |
|
|
|
/** |
|
* @param \ReflectionMethod $reflectionMethod |
|
* |
|
* @return array|null |
|
*/ |
|
private function methodResolve(\ReflectionMethod $reflectionMethod): ?array |
|
{ |
|
if (!$reflectionMethod->getAttributes(Api::class)) { |
|
return null; |
|
} |
|
$routeKey = $this->getCallLocation($reflectionMethod); |
|
$route = $this->allRouteMap[$routeKey]; |
|
|
|
$api = [ |
|
'name' => $this->apiTitleResolve($reflectionMethod->getDocComment()), |
|
'call' => $routeKey, |
|
'url' => $route['rule'], |
|
'method' => strtoupper($route['method']), |
|
'version' => [1], |
|
'auth' => $this->authenticateType($route), |
|
'requestParams' => [], |
|
'responseParams' => [], |
|
]; |
|
|
|
foreach ($reflectionMethod->getAttributes() as $attribute) { |
|
$attribute = $attribute->newInstance(); |
|
switch (true) { |
|
case $attribute instanceof ApiQuery: |
|
case $attribute instanceof ApiBody: |
|
$api['requestParams'][] = json_decode(json_encode($attribute), true); |
|
break; |
|
case $attribute instanceof ApiReturn: |
|
$api['responseParams'][] = json_decode(json_encode($attribute), true); |
|
break; |
|
case $attribute instanceof ApiVersion: |
|
$api['version'][] = $attribute->version; |
|
} |
|
} |
|
$api['responseParams'] = $this->childrenParamHandle($api['responseParams']); |
|
if ($api['method'] === 'POST') { |
|
$api['requestParams'] = $this->childrenParamHandle($api['requestParams']); |
|
} |
|
|
|
return $api; |
|
} |
|
|
|
/** |
|
* @param array $initialData |
|
* |
|
* @return array |
|
*/ |
|
private function childrenParamHandle(array $initialData): array |
|
{ |
|
$newData = []; |
|
foreach ($initialData as $item) { |
|
$item['children'] = []; |
|
$names = explode('.', $item['name']); |
|
$current = &$newData; |
|
|
|
foreach (array_slice($names, 0, -1) as $attr) { |
|
$current = &$current[$attr]['children']; |
|
} |
|
|
|
$item['name'] = end($names); |
|
$current[$item['name']] = $item; |
|
} |
|
|
|
return $this->childrenData(array_values($newData)); |
|
} |
|
|
|
/** |
|
* @param array $data |
|
* |
|
* @return array |
|
*/ |
|
private function childrenData(array $data): array |
|
{ |
|
return array_map(function ($value){ |
|
if ($value['children']){ |
|
$value['children'] = array_values($this->childrenData($value['children'])); |
|
} |
|
return $value; |
|
}, $data); |
|
} |
|
|
|
/** |
|
* @param \ReflectionMethod $method |
|
* |
|
* @return string |
|
*/ |
|
private function getCallLocation(\ReflectionMethod $method): string |
|
{ |
|
return $method->getDeclaringClass()->getNamespaceName() . "\\" |
|
. $method->getDeclaringClass()->getShortName() . "@" |
|
. $method->getName(); |
|
} |
|
|
|
/** |
|
* @param mixed $path |
|
* |
|
* @return array|string[] |
|
*/ |
|
private function getPaths(mixed $path): array |
|
{ |
|
if (str_contains($path, '*')) { |
|
$MPaths = []; |
|
$paths = explode('*', $path); |
|
$last = array_pop($paths); |
|
foreach ($paths as $mp) { |
|
if (!$MPaths) { |
|
$dirs = Tool::dir($mp)->getDirs(); |
|
$MPaths = array_map(fn($dir) => $mp . DIRECTORY_SEPARATOR . $dir, $dirs); |
|
continue; |
|
} |
|
$MPaths = array_merge(...array_map(function ($tp) use ($mp) { |
|
if (!is_dir($tp . DIRECTORY_SEPARATOR . $mp)) { |
|
return []; |
|
} |
|
|
|
return Tool::dir($tp . DIRECTORY_SEPARATOR . $mp)->getDirs(); |
|
}, $MPaths)); |
|
} |
|
|
|
$MPaths = array_filter(array_map(fn($tp) => is_dir($tp . DIRECTORY_SEPARATOR . $last) ? $tp . DIRECTORY_SEPARATOR . $last : '', $MPaths)); |
|
} else { |
|
$MPaths = [$path]; |
|
} |
|
return array_map(fn($path) => preg_replace('/(\/+|\\\+)/', DIRECTORY_SEPARATOR, $path),$MPaths); |
|
} |
|
} |