Api文档
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

<?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);
}
}