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