├── .gitignore ├── src ├── config │ └── plugin │ │ └── hg │ │ └── apidoc │ │ ├── route.php │ │ └── app.php ├── middleware │ ├── LaravelMiddleware.php │ ├── ThinkPHPMiddleware.php │ ├── HyperfMiddleware.php │ └── WebmanMiddleware.php ├── annotation │ ├── NotParams.php │ ├── NotQuerys.php │ ├── NotResponses.php │ ├── NotDefaultAuthor.php │ ├── NotHeaders.php │ ├── NotResponseError.php │ ├── NotResponseSuccess.php │ ├── NotDebug.php │ ├── Group.php │ ├── Sort.php │ ├── Tag.php │ ├── NotParse.php │ ├── Url.php │ ├── ContentType.php │ ├── Field.php │ ├── Author.php │ ├── Desc.php │ ├── Title.php │ ├── WithoutField.php │ ├── ParamType.php │ ├── RouteMiddleware.php │ ├── Method.php │ ├── Md.php │ ├── ResponseErrorMd.php │ ├── ResponseSuccessMd.php │ ├── ResponseStatus.php │ ├── Header.php │ ├── RouteParam.php │ ├── EventBase.php │ ├── ResponseError.php │ ├── After.php │ ├── ParamBase.php │ ├── Param.php │ ├── ResponseSuccess.php │ ├── Before.php │ ├── Query.php │ ├── Property.php │ ├── Returned.php │ └── AddField.php ├── ConfigProvider.php ├── exception │ ├── HttpException.php │ └── ErrorException.php ├── utils │ ├── ApiCrossDomain.php │ ├── AbstractAnnotation.php │ ├── Request.php │ ├── Lang.php │ ├── ConfigProvider.php │ ├── AutoRegisterRouts.php │ ├── ApiShare.php │ ├── Cache.php │ └── DirAndFile.php ├── providers │ ├── CommonService.php │ ├── WebmanService.php │ ├── HyperfService.php │ ├── ThinkPHP5Service.php │ ├── LaravelService.php │ ├── ThinkPHPService.php │ └── BaseService.php ├── Install.php ├── parses │ ├── ParseCodeTemplate.php │ ├── ParseMarkdown.php │ ├── ParseModel.php │ ├── ParseAnnotation.php │ └── ParseApiMenus.php ├── config.php ├── Auth.php ├── generator │ ├── ParseTemplate.php │ └── Index.php └── Controller.php ├── LICENSE ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /src/config/plugin/hg/apidoc/route.php: -------------------------------------------------------------------------------- 1 | all(); 12 | $config = ConfigProvider::get(); 13 | $config['request_params'] = $params; 14 | ConfigProvider::set($config); 15 | return $next($request); 16 | } 17 | } -------------------------------------------------------------------------------- /src/middleware/ThinkPHPMiddleware.php: -------------------------------------------------------------------------------- 1 | param(); 12 | $config = ConfigProvider::get(); 13 | $config['request_params'] = $params; 14 | ConfigProvider::set($config); 15 | return $next($request); 16 | } 17 | } -------------------------------------------------------------------------------- /src/annotation/NotParams.php: -------------------------------------------------------------------------------- 1 | [], 14 | 'publish' => [ 15 | [ 16 | 'id' => 'config', 17 | 'description' => 'The config of apidoc.', 18 | 'source' => __DIR__ . '/config.php', 19 | 'destination' => BASE_PATH . '/config/autoload/apidoc.php', 20 | ], 21 | ], 22 | ]; 23 | } 24 | } -------------------------------------------------------------------------------- /src/annotation/RouteMiddleware.php: -------------------------------------------------------------------------------- 1 | statusCode = $statusCode; 19 | $this->headers = $headers; 20 | 21 | parent::__construct($message, $code, $previous); 22 | } 23 | 24 | public function getStatusCode() 25 | { 26 | return $this->statusCode; 27 | } 28 | 29 | public function getHeaders() 30 | { 31 | return $this->headers; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/annotation/ResponseErrorMd.php: -------------------------------------------------------------------------------- 1 | server('HTTP_ORIGIN') ? $request->server('HTTP_ORIGIN') : ''; 12 | $response->header('Access-Control-Allow-Origin', $origin); 13 | $response->header('Access-Control-Allow-Headers', 'Origin, Content-Type, Cookie, X-CSRF-TOKEN, Accept, Authorization, X-XSRF-TOKEN'); 14 | $response->header('Access-Control-Expose-Headers', 'Authorization, authenticated'); 15 | $response->header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, OPTIONS'); 16 | $response->header('Access-Control-Allow-Credentials', 'true'); 17 | return $response; 18 | 19 | } 20 | 21 | 22 | } -------------------------------------------------------------------------------- /src/utils/AbstractAnnotation.php: -------------------------------------------------------------------------------- 1 | formatParams($value); 17 | foreach ($formattedValue as $key => $val) { 18 | if ($key=="value" && !property_exists($this, $key)){ 19 | $this->name = $val; 20 | }else{ 21 | $this->{$key} = $val; 22 | } 23 | } 24 | } 25 | 26 | protected function formatParams($value): array 27 | { 28 | if (isset($value[0])) { 29 | $value = $value[0]; 30 | } 31 | if (!is_array($value)) { 32 | $value = ['name' => $value]; 33 | } 34 | return $value; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/annotation/ResponseStatus.php: -------------------------------------------------------------------------------- 1 | get = $_GET; 16 | $this->post = $_POST; 17 | $this->method = !empty($_SERVER['REQUEST_METHOD'])?$_SERVER['REQUEST_METHOD']:""; 18 | } 19 | 20 | public function get(){ 21 | return $this->get; 22 | } 23 | 24 | public function post(){ 25 | return $this->post; 26 | } 27 | 28 | public function input(){ 29 | $input = file_get_contents('php://input'); 30 | $inputObj = json_decode($input); 31 | return Helper::objectToArray($inputObj); 32 | } 33 | 34 | public function param(){ 35 | $config = ConfigProvider::get(); 36 | if (!empty($config['request_params'])){ 37 | return $config['request_params']; 38 | } 39 | $method = !empty($this->method)?$this->method:"GET"; 40 | if ($method == "GET"){ 41 | return $this->get; 42 | } 43 | return $this->input(); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /src/annotation/RouteParam.php: -------------------------------------------------------------------------------- 1 | 'config/plugin/hg/apidoc', 15 | ); 16 | 17 | /** 18 | * Install 19 | * @return void 20 | */ 21 | public static function install() 22 | { 23 | foreach (static::$configPath as $source => $dest) { 24 | if ($pos = strrpos($dest, '/')) { 25 | $parent_dir = base_path() . '/' . substr($dest, 0, $pos); 26 | if (!is_dir($parent_dir)) { 27 | mkdir($parent_dir, 0777, true); 28 | } 29 | } 30 | //symlink(__DIR__ . "/$source", base_path()."/$dest"); 31 | copy_dir(__DIR__ . "/$source", base_path() . "/$dest"); 32 | echo "Create $dest"; 33 | } 34 | } 35 | 36 | /** 37 | * Uninstall 38 | * @return void 39 | */ 40 | public static function uninstall() 41 | { 42 | foreach (static::$configPath as $source => $dest) { 43 | $path = base_path()."/$dest"; 44 | if (!is_dir($path) && !is_file($path)) { 45 | continue; 46 | } 47 | echo "Remove $dest"; 48 | if (is_file($path) || is_link($path)) { 49 | unlink($path); 50 | continue; 51 | } 52 | remove_dir($path); 53 | } 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/annotation/After.php: -------------------------------------------------------------------------------- 1 | 1 ? trim($key[1]):""; 33 | if (!empty($langKey)){ 34 | return $langGetFunction($langKey); 35 | } 36 | } 37 | } 38 | return $string; 39 | } 40 | 41 | /** 42 | * 二维数组设置指定字段的多语言 43 | * @param $array 44 | * @param $field 45 | * @return array 46 | */ 47 | public static function getArrayLang($array,$field,$config=[]){ 48 | $data = []; 49 | if (!empty($array) && is_array($array)){ 50 | foreach ($array as $item){ 51 | $item[$field] = static::getLang($item[$field],$config); 52 | $data[]=$item; 53 | } 54 | } 55 | return $data; 56 | } 57 | 58 | 59 | } -------------------------------------------------------------------------------- /src/annotation/Before.php: -------------------------------------------------------------------------------- 1 | middleware([WebmanMiddleware::class]); 19 | }); 20 | 21 | // 自动注册路由 22 | CommonService::autoRegisterRoutes(function ($routeData){ 23 | foreach ($routeData as $controller) { 24 | if (count($controller['methods'])){ 25 | $methods= $controller['methods']; 26 | $routeCallback = function ()use ($methods){ 27 | foreach ($methods as $method) { 28 | $apiMethods = Helper::handleApiMethod($method['method']); 29 | $route = Route::add([...$apiMethods,'OPTIONS'],$method['url'], $method['controller']."@".$method['name']); 30 | if (!empty($method['middleware'])){ 31 | $route->middleware($method['middleware']); 32 | } 33 | } 34 | }; 35 | $routeGroup = Route::group("",$routeCallback); 36 | if (!empty($controller['middleware'])){ 37 | $routeGroup->middleware($controller['middleware']); 38 | } 39 | } 40 | } 41 | }, WebmanMiddleware::getApidocConfig()); 42 | } 43 | } -------------------------------------------------------------------------------- /src/annotation/AddField.php: -------------------------------------------------------------------------------- 1 | [HyperfMiddleware::class]]); 26 | }); 27 | 28 | // 自动注册路由 29 | CommonService::autoRegisterRoutes(function ($routeData){ 30 | foreach ($routeData as $controller) { 31 | if (count($controller['methods'])){ 32 | $methods= $controller['methods']; 33 | $routeCallback = function ()use ($methods){ 34 | foreach ($methods as $method) { 35 | $apiMethods = Helper::handleApiMethod($method['method']); 36 | $options = []; 37 | if (!empty($method['middleware'])){ 38 | $options['middleware']= $method['middleware']; 39 | } 40 | Router::addRoute([...$apiMethods,'OPTIONS'],$method['url'], $method['controller']."@".$method['name'],$options); 41 | } 42 | }; 43 | $groupOptions = []; 44 | if (!empty($controller['middleware'])){ 45 | $groupOptions['middleware'] = $controller['middleware']; 46 | } 47 | Router::addGroup("",$routeCallback,$groupOptions); 48 | } 49 | } 50 | }, HyperfMiddleware::getApidocConfig()); 51 | } 52 | } -------------------------------------------------------------------------------- /src/middleware/HyperfMiddleware.php: -------------------------------------------------------------------------------- 1 | initConfig(); 22 | 23 | if ($request->getMethod() == "GET"){ 24 | $params = $request->getQueryParams(); 25 | }else{ 26 | $params = $request->getParsedBody(); 27 | } 28 | $config = ConfigProvider::get(); 29 | $config['request_params'] = $params; 30 | ConfigProvider::set($config); 31 | return $handler->handle($request); 32 | } 33 | 34 | static function getApidocConfig() 35 | { 36 | $config = config("apidoc"); 37 | $exportConfig = config("apidoc-export"); 38 | if (!(!empty($config['auto_url']) && !empty($config['auto_url']['filter_keys']))){ 39 | $config['auto_url']['filter_keys'] = ['App','Controller']; 40 | } 41 | $config['app_frame'] = "hyperf"; 42 | if (!empty($exportConfig)){ 43 | $config['export_config'] = $exportConfig; 44 | } 45 | return $config; 46 | } 47 | 48 | static function registerRoute($route) 49 | { 50 | // TODO: Implement registerRoute() method. 51 | } 52 | 53 | static function databaseQuery($sql) 54 | { 55 | return Db::select($sql); 56 | } 57 | 58 | static function getRootPath() 59 | { 60 | return BASE_PATH."/"; 61 | } 62 | 63 | static function getRuntimePath() 64 | { 65 | return BASE_PATH."/runtime/"; 66 | } 67 | 68 | static function setLang($locale) 69 | { 70 | static::$langLocale = $locale; 71 | } 72 | 73 | static function getLang($lang): string 74 | { 75 | return trans($lang); 76 | } 77 | 78 | static function handleResponseJson($res) 79 | { 80 | return $res; 81 | } 82 | 83 | static function getTablePrefix(){ 84 | return config('databases.default.prefix',''); 85 | } 86 | } -------------------------------------------------------------------------------- /src/middleware/WebmanMiddleware.php: -------------------------------------------------------------------------------- 1 | initConfig(); 19 | $params = $request->all(); 20 | $config = ConfigProvider::get(); 21 | $config['request_params'] = $params; 22 | ConfigProvider::set($config); 23 | 24 | $response = $request->method() == 'OPTIONS' ? response('') : $handler($request); 25 | if (!empty($config['allowCrossDomain'])){ 26 | // 给响应添加跨域相关的http头 27 | $response->withHeaders([ 28 | 'Access-Control-Allow-Credentials' => 'true', 29 | 'Access-Control-Allow-Origin' => $request->header('origin', '*'), 30 | 'Access-Control-Allow-Methods' => $request->header('access-control-request-method', '*'), 31 | 'Access-Control-Allow-Headers' => $request->header('access-control-request-headers', '*'), 32 | ]); 33 | } 34 | 35 | return $response; 36 | } 37 | 38 | static function getApidocConfig() 39 | { 40 | $config = config('plugin.hg.apidoc.app.apidoc'); 41 | $exportConfig = config('plugin.hg.apidoc-export.app'); 42 | if (!(!empty($config['auto_url']) && !empty($config['auto_url']['filter_keys']))){ 43 | $config['auto_url']['filter_keys'] = ['app','controller']; 44 | } 45 | $config['app_frame'] = "webman"; 46 | if (!empty($exportConfig)){ 47 | $config['export_config'] = $exportConfig; 48 | } 49 | return $config; 50 | } 51 | 52 | static function registerRoute($route) 53 | { 54 | return ""; 55 | } 56 | 57 | static function databaseQuery($sql) 58 | { 59 | return Db::select($sql); 60 | } 61 | 62 | static function getRootPath() 63 | { 64 | return BASE_PATH."/"; 65 | } 66 | 67 | static function getRuntimePath() 68 | { 69 | return BASE_PATH."/runtime/"; 70 | } 71 | 72 | static function setLang($locale) 73 | { 74 | locale($locale); 75 | } 76 | 77 | static function getLang($lang): string 78 | { 79 | return $lang; 80 | } 81 | 82 | static function handleResponseJson($res) 83 | { 84 | return json($res); 85 | } 86 | 87 | static function getTablePrefix(){ 88 | $driver = config('database.default'); 89 | $table_prefix=config('database.connections.'.$driver.'.prefix'); 90 | return $table_prefix; 91 | } 92 | } -------------------------------------------------------------------------------- /src/providers/ThinkPHP5Service.php: -------------------------------------------------------------------------------- 1 | initConfig(); 19 | self::registerApidocRoutes(); 20 | // 自动注册路由 21 | self::autoRegisterRoutes(function ($routeData){ 22 | $appRoute = app('route'); 23 | $routeGroup = $appRoute->getGroup(); 24 | foreach ($routeData as $controller) { 25 | $routeGroup = $appRoute->getGroup(); 26 | if (!empty($controller['middleware'])){ 27 | $routeGroup->middleware($controller['middleware']); 28 | } 29 | if (count($controller['methods'])){ 30 | foreach ($controller['methods'] as $method) { 31 | $apiMethods = Helper::handleApiMethod($method['method']); 32 | $apiMethods = implode("|",$apiMethods); 33 | $route = $routeGroup->addRule($method['url'],$method['controller']."@".$method['name'],$apiMethods); 34 | if (!empty($method['middleware'])){ 35 | $route->middleware($method['middleware']); 36 | } 37 | } 38 | } 39 | } 40 | }); 41 | } 42 | 43 | static function getApidocConfig() 44 | { 45 | $config = config("apidoc."); 46 | $exportConfig = config("apidoc-export."); 47 | if (!(!empty($config['auto_url']) && !empty($config['auto_url']['filter_keys']))){ 48 | $config['auto_url']['filter_keys'] = ['app','controller']; 49 | } 50 | $config['app_frame'] = "thinkphp5"; 51 | if (!empty($exportConfig)){ 52 | $config['export_config'] = $exportConfig; 53 | } 54 | return $config; 55 | } 56 | 57 | static function registerRoute($route){ 58 | $config = self::getApidocConfig(); 59 | $registerRoute = Route::rule($route['uri'], $route['callback'],"*"); 60 | if (!empty($config['allowCrossDomain'])) { 61 | $registerRoute->allowCrossDomain(); 62 | } 63 | } 64 | 65 | static function databaseQuery($sql){ 66 | return Db::query($sql); 67 | } 68 | 69 | static function getTablePrefix(){ 70 | $driver = config('database.default'); 71 | $table_prefix=config('database.connections.'.$driver.'.prefix'); 72 | return $table_prefix; 73 | } 74 | 75 | static function getRootPath() 76 | { 77 | return App::getRootPath(); 78 | } 79 | 80 | static function getRuntimePath() 81 | { 82 | return App::getRuntimePath(); 83 | } 84 | 85 | static function setLang($locale){ 86 | Lang::setLangCookieVar($locale); 87 | } 88 | 89 | static function getLang($lang){ 90 | return Lang::get($lang); 91 | } 92 | 93 | static function handleResponseJson($res){ 94 | return json($res); 95 | } 96 | 97 | 98 | } 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |

6 | Apidoc 7 |

8 | 9 |
10 | 基于PHP的注解生成API文档及Api接口开发工具 11 |
12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | ## 🤷‍♀️ Apidoc是什么? 26 | 27 | Apidoc是一个通过解析注解生成Api接口文档的PHP composer扩展,兼容Laravel、ThinkPHP、Hyperf、Webman等框架; 28 | 全面的注解引用、数据表字段引用,简单的注解即可生成Api文档,而Apidoc不仅于接口文档,在线接口调试、Mock调试数据、调试事件处理、Json/TypeScript生成、接口生成器、代码生成器等诸多实用功能,致力于提高Api接口开发效率。 29 | 30 | 31 | ## ✨特性 32 | 33 | - 开箱即用:无繁杂的配置、安装后按文档编写注释即可自动生成API文档。 34 | - 轻松编写:支持通用注释引用、业务逻辑层、数据表字段的引用,几句注释即可完成。 35 | - 在线调试:在线文档可直接调试,并支持全局请求/Mock参数/事件处理,接口调试省时省力。 36 | - 安全高效:支持访问密码验证、应用/版本独立密码;支持文档缓存。 37 | - 多应用/多版本:可适应各种单应用、多应用、多版本的项目的Api管理。 38 | - 分组/Tag:可对控制器/接口进行多级分组或定义Tag。 39 | - Markdown文档:支持.md文件的文档展示。 40 | - Json/TypeScript生成:文档自动生成接口的Json及TypeScript。 41 | - 代码生成器:配置+模板即可快速生成代码及数据表的创建,大大提高工作效率。 42 | - 接口分享:支持自由指定应用/接口生成分享链接、导出swagger.json文件。 43 | 44 | ## 📌兼容 45 | 46 | 以下框架已内置兼容,可开箱即用 47 | 48 | | 框架 | 版本 | 说明 | 49 | | -------- |--------| ------------------------------------ | 50 | | ThinkPHP | \>=5.1 | | 51 | | Webman | \>=1.x | | 52 | | Laravel | \>=8.x | 低于 Laravel8 版本未测试,可自行尝试 | 53 | | Hyperf | \>=2.x | | 54 | 55 | 56 | ## 📖使用文档 57 | 58 | [https://docs.apidoc.icu](https://docs.apidoc.icu/) 59 | 60 | 61 | ## 🏆支持我们 62 | 63 | 如果本项目对您有所帮助,请点个Star支持我们 64 | 65 | - [Github](https://github.com/HGthecode/apidoc-php) -> 66 | Star me on GitHub 67 | 68 | - [Gitee](https://gitee.com/hg-code/apidoc-php) -> star 69 | 70 | 71 | ## 🌐交流群 72 | 73 | ![QQ群](https://docs.apidoc.icu/qq-qun.png) 74 | 75 | 76 | 77 | ## 💡鸣谢 78 | 79 | doctrine/annotations 80 | 81 | 82 | ## 🔗链接 83 | ApiDoc UI 84 | 85 | ApiDoc Demo 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/parses/ParseCodeTemplate.php: -------------------------------------------------------------------------------- 1 | config = $config; 25 | } 26 | 27 | public function renderCode($params) 28 | { 29 | $appKey = $params['appKey']; 30 | $currentAppConfig = Helper::getCurrentAppConfig($appKey); 31 | $currentApp = $currentAppConfig['appConfig']; 32 | $this->currentApp = $currentApp; 33 | 34 | $codeTemplate = $params['template']; 35 | 36 | //验证参数 37 | 38 | //验证模板文件是否存在 39 | 40 | //解析接口数据 41 | $tplData = []; 42 | if ($codeTemplate['select_mode'] == 'controller'){ 43 | $parseApiMenusService = new ParseApiMenus($this->config); 44 | $controllers = $params['selected']; 45 | if (!empty($controllers) && count($controllers) > 0) { 46 | $controllerList = []; 47 | foreach ($controllers as $class) { 48 | $classData = $parseApiMenusService->parseController($class); 49 | if ($classData !== false) { 50 | $controllerList[] = $classData; 51 | } 52 | } 53 | if (empty($codeTemplate['multiple'])){ 54 | $tplData = $controllerList[0]; 55 | }else{ 56 | $tplData = $controllerList; 57 | } 58 | } 59 | }else{ 60 | // api 61 | $apis = $params['selected']; 62 | if (!empty($apis) && count($apis) > 0) { 63 | $parseApiDetailService = new ParseApiDetail($this->config); 64 | $apiList = []; 65 | foreach ($apis as $key) { 66 | $apiKey = urldecode($key); 67 | $apiDetail = $parseApiDetailService->renderApiDetail($appKey,$apiKey); 68 | if ($apiDetail !== false) { 69 | $apiList[] = $apiDetail; 70 | } 71 | } 72 | if (empty($codeTemplate['multiple'])){ 73 | $tplData = $apiList[0]; 74 | }else{ 75 | $tplData = $apiList; 76 | } 77 | } 78 | } 79 | 80 | 81 | // 读取模板 82 | $templatePath =DirAndFile::formatPath( APIDOC_ROOT_PATH . $codeTemplate['template'],"/"); 83 | if (is_readable($templatePath) == false) { 84 | throw new ErrorException("template not found", [ 85 | 'template' => $template 86 | ]); 87 | } 88 | $tplParams = [ 89 | 'form'=> $params['form'], 90 | 'data'=>$tplData 91 | ]; 92 | $html = (new ParseTemplate())->compile($templatePath,$tplParams); 93 | 94 | 95 | 96 | return $html; 97 | } 98 | 99 | 100 | } -------------------------------------------------------------------------------- /src/exception/ErrorException.php: -------------------------------------------------------------------------------- 1 | ['status'=>404,'code' => 4004, 'msg' => '文档已关闭'], 15 | 'password error' => ['status'=>402,'code' => 4002, 'msg' => '密码不正确,请重新输入'], 16 | 'password not found' => ['status'=>402,'code' => 4002, 'msg' => '密码不可为空'], 17 | 'token error' => ['status'=>401,'code' => 4001, 'msg' => '不合法的Token'], 18 | 'token not found' => ['status'=>401,'code' => 4001, 'msg' => '不存在Token'], 19 | 'appkey not found' => ['status'=>412,'code' => 4005, 'msg' => '缺少必要参数appKey'], 20 | 'mdPath not found' => ['status'=>412,'code' => 4006, 'msg' => '缺少必要参数path'], 21 | 'appKey error' => ['status'=>412,'code' => 4007, 'msg' => '不存在 key为${appKey}的apps配置'], 22 | 'template not found' => ['status'=>412,'code' => 4008, 'msg' => '${template}模板不存在'], 23 | 'path not found' => ['status'=>412,'code' => 4009, 'msg' => '${path}目录不存在'], 24 | 'classname error' => ['status'=>412,'code' => 4010, 'msg' => '${classname}文件名不合法'], 25 | 'apiKey not found' => ['status'=>412,'code' => 4011, 'msg' => '缺少必要参数apiKey'], 26 | 'no config apps' => ['status'=>412,'code' => 5000, 'msg' => 'apps配置不可为空'], 27 | 'unknown error' => ['status'=>501,'code' => 5000, 'msg' => '未知异常'], 28 | 'no debug' => ['status'=>412,'code' => 5001, 'msg' => '请在debug模式下,使用该功能'], 29 | 'no config crud' => ['status'=>412,'code' => 5002, 'msg' => 'crud未配置'], 30 | 'datatable crud error' => ['status'=>412,'code' => 5003, 'msg' => '数据表创建失败,请检查配置'], 31 | 'file already exists' => ['status'=>412,'code' => 5004, 'msg' => '${filepath}文件已存在'], 32 | 'file not exists' => ['status'=>412,'code' => 5005, 'msg' => '${filepath}文件不存在'], 33 | 'datatable already exists' => ['status'=>412,'code' => 5004, 'msg' => '数据表${table}已存在'], 34 | 'datatable not exists' => ['status'=>412,'code' => 5004, 'msg' => '数据表${table}不存在'], 35 | 'ref file not exists' => ['status'=>412,'code' => 5005, 'msg' => 'ref引入 ${path} 文件不存在'], 36 | 'ref method not exists' => ['status'=>412,'code' => 5005, 'msg' => 'ref引入${path} 中 ${method} 方法不存在'], 37 | 'datatable create error' => ['status'=>412,'code' => 5006, 'msg' => '数据表[${table}]创建失败,error:${message},sql:${sql}'], 38 | 'field not found' => ['status'=>412,'code' => 5006, 'msg' => '${field}字段不能为空'], 39 | 'share not exists' => ['status'=>404,'code' => 4004, 'msg' => '该分享不存在'], 40 | 'export config not enable' => ['status'=>404,'code' => 4004, 'msg' => '导出配置未启用'], 41 | ]; 42 | 43 | public function __construct(string $exceptionCode, array $data = []) 44 | { 45 | $config = ConfigProvider::get(); 46 | $exception = $this->getException($exceptionCode); 47 | if ($exception){ 48 | $msg = Helper::replaceTemplate($exception['msg'], $data); 49 | }else{ 50 | $exception = $this->exceptions['unknown error']; 51 | $msg = $exceptionCode; 52 | } 53 | parent::__construct($exception['status'], $msg, null, [], $exception['code']); 54 | 55 | } 56 | 57 | public function getException($exceptionCode) 58 | { 59 | if (isset($this->exceptions[$exceptionCode])) { 60 | return $this->exceptions[$exceptionCode]; 61 | } 62 | return null; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/config.php: -------------------------------------------------------------------------------- 1 | 'Apidoc', 5 | // (选配)文档描述,显示在首页 6 | 'desc' => '', 7 | // (必须)设置文档的应用/版本 8 | 'apps' => [ 9 | [ 10 | // (必须)标题 11 | 'title'=>'Api接口', 12 | // (必须)控制器目录地址 13 | 'path'=>'app\controller', 14 | // (必须)唯一的key 15 | 'key'=>'api', 16 | ] 17 | ], 18 | 19 | // (必须)指定通用注释定义的文件地址 20 | 'definitions' => "app\common\controller\Definitions", 21 | // (必须)自动生成url规则,当接口不添加@Apidoc\Url ("xxx")注解时,使用以下规则自动生成 22 | 'auto_url' => [ 23 | // 字母规则,lcfirst=首字母小写;ucfirst=首字母大写; 24 | 'letter_rule' => "lcfirst", 25 | // url前缀 26 | 'prefix'=>"", 27 | ], 28 | // 是否自动注册路由 29 | 'auto_register_routes'=>false, 30 | // (必须)缓存配置 31 | 'cache' => [ 32 | // 是否开启缓存 33 | 'enable' => false, 34 | ], 35 | // (必须)权限认证配置 36 | 'auth' => [ 37 | // 是否启用密码验证 38 | 'enable' => false, 39 | // 全局访问密码 40 | 'password' => "123456", 41 | // 密码加密盐 42 | 'secret_key' => "apidoc#hg_code", 43 | // 授权访问后的有效期 44 | 'expire' => 24*60*60 45 | ], 46 | // 全局参数 47 | 'params'=>[ 48 | // (选配)全局的请求Header 49 | 'header'=>[ 50 | // name=字段名,type=字段类型,require=是否必须,default=默认值,desc=字段描述 51 | ['name'=>'Authorization','type'=>'string','require'=>true,'desc'=>'身份令牌Token'], 52 | ], 53 | // (选配)全局的请求Query 54 | 'query'=>[ 55 | // 同上 header 56 | ], 57 | // (选配)全局的请求Body 58 | 'body'=>[ 59 | // 同上 header 60 | ], 61 | ], 62 | // 全局响应体 63 | 'responses'=>[ 64 | // 成功响应体 65 | 'success'=>[ 66 | ['name'=>'code','desc'=>'业务代码','type'=>'int','require'=>1], 67 | ['name'=>'message','desc'=>'业务信息','type'=>'string','require'=>1], 68 | //参数同上 headers;main=true来指定接口Returned参数挂载节点 69 | ['name'=>'data','desc'=>'业务数据','main'=>true,'type'=>'object','require'=>1], 70 | ], 71 | // 异常响应体 72 | 'error'=>[ 73 | ['name'=>'code','desc'=>'业务代码','type'=>'int','require'=>1,'md'=>'/docs/HttpError.md'], 74 | ['name'=>'message','desc'=>'业务信息','type'=>'string','require'=>1], 75 | ] 76 | ], 77 | // (选配)全局响应状态码 78 | 'responses_status'=>[ 79 | [ 80 | 'name'=>'200', 81 | 'desc'=>'请求成功' 82 | ], 83 | [ 84 | 'name'=>'401', 85 | 'desc'=>'登录令牌无效', 86 | 'contentType'=>'' 87 | ], 88 | ], 89 | // (选配)apidoc路由前缀,默认apidoc 90 | 'route_prefix'=>'/apidoc', 91 | //(选配)默认作者 92 | 'default_author'=>'', 93 | //(选配)默认请求类型 94 | 'default_method'=>'GET', 95 | //(选配)允许跨域访问 96 | 'allowCrossDomain'=>false, 97 | /** 98 | * (选配)解析时忽略带@注解的关键词,当注解中存在带@字符并且非Apidoc注解,如 @key test,此时Apidoc页面报类似以下错误时: 99 | * [Semantical Error] The annotation "@key" in method xxx() was never imported. Did you maybe forget to add a "use" statement for this annotation? 100 | */ 101 | 'ignored_annitation'=>[], 102 | 103 | // (选配)解析时忽略的方法 104 | 'ignored_methods'=>[], 105 | 106 | // (选配)数据库配置 107 | 'database'=>[], 108 | // (选配)Markdown文档 109 | 'docs' => [], 110 | // (选配)代码生成器配置 注意:是一个二维数组 111 | 'generator' =>[], 112 | // (选配)代码模板 113 | 'code_template'=>[], 114 | // (选配)接口分享功能 115 | 'share'=>[ 116 | // 是否开启接口分享功能 117 | 'enable'=>false, 118 | // 自定义接口分享操作,二维数组,每个配置为一个按钮操作 119 | 'actions'=>[] 120 | ], 121 | ]; 122 | -------------------------------------------------------------------------------- /src/providers/LaravelService.php: -------------------------------------------------------------------------------- 1 | publishes([ 23 | __DIR__.'/../config.php' => config_path('apidoc.php'), 24 | ]); 25 | } 26 | 27 | public function register() 28 | { 29 | $config = static::getApidocConfig(); 30 | $this->initConfig(); 31 | self::registerApidocRoutes(); 32 | 33 | // 自动注册路由 34 | self::autoRegisterRoutes(function ($routeData){ 35 | foreach ($routeData as $controller) { 36 | if (count($controller['methods'])){ 37 | $methods= $controller['methods']; 38 | $routeCallback = function ()use ($methods){ 39 | foreach ($methods as $method) { 40 | $apiMethods = Helper::handleApiMethod($method['method']); 41 | $route = Route::match($apiMethods + ['OPTIONS'],$method['url'], "\\".$method['controller']."@".$method['name']); 42 | if (!empty($method['middleware'])){ 43 | $route->middleware($method['middleware']); 44 | } 45 | } 46 | }; 47 | $routeGroup = Route::prefix(""); 48 | if (!empty($controller['middleware'])){ 49 | $routeGroup->middleware($controller['middleware']); 50 | } 51 | $routeGroup->group($routeCallback); 52 | } 53 | } 54 | }); 55 | 56 | } 57 | 58 | static function getApidocConfig() 59 | { 60 | $config = config("apidoc"); 61 | $exportConfig = config("apidoc-export"); 62 | if (!(!empty($config['auto_url']) && !empty($config['auto_url']['filter_keys']))){ 63 | $config['auto_url']['filter_keys'] = ['App','Http','Controllers']; 64 | } 65 | $config['app_frame'] = "laravel"; 66 | if (!empty($exportConfig)){ 67 | $config['export_config'] = $exportConfig; 68 | } 69 | return $config; 70 | } 71 | 72 | static function registerRoute($route){ 73 | $config = self::getApidocConfig(); 74 | $registerRoute = Route::any($route['uri'], $route['callback']); 75 | $registerRoute->middleware([LaravelMiddleware::class]); 76 | if (!empty($config['allowCrossDomain'])) { 77 | $registerRoute->middleware([ApiCrossDomain::class]); 78 | } 79 | } 80 | 81 | static function databaseQuery($sql){ 82 | return DB::select($sql); 83 | } 84 | 85 | static function getTablePrefix(){ 86 | $driver = config('database.default'); 87 | $table_prefix=config('database.connections.'.$driver.'.prefix'); 88 | return $table_prefix; 89 | } 90 | 91 | static function getRootPath() 92 | { 93 | return base_path()."/"; 94 | } 95 | 96 | static function getRuntimePath() 97 | { 98 | return storage_path()."/"; 99 | } 100 | 101 | static function setLang($locale){ 102 | Lang::setLocale($locale); 103 | } 104 | 105 | static function getLang($lang){ 106 | return trans($lang); 107 | } 108 | 109 | static function handleResponseJson($res){ 110 | return $res; 111 | } 112 | 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/config/plugin/hg/apidoc/app.php: -------------------------------------------------------------------------------- 1 | true, 4 | 'apidoc' => [ 5 | // (选配)文档标题,显示在左上角与首页 6 | 'title' => 'Apidoc', 7 | // (选配)文档描述,显示在首页 8 | 'desc' => '', 9 | // (必须)设置文档的应用/版本 10 | 'apps' => [ 11 | [ 12 | // (必须)标题 13 | 'title'=>'Api接口', 14 | // (必须)控制器目录地址 15 | 'path'=>'app\controller', 16 | // (必须)唯一的key 17 | 'key'=>'api', 18 | ] 19 | ], 20 | // (必须)指定通用注释定义的文件地址 21 | 'definitions' => "app\common\controller\Definitions", 22 | // (必须)自动生成url规则,当接口不添加@Apidoc\Url ("xxx")注解时,使用以下规则自动生成 23 | 'auto_url' => [ 24 | // 字母规则,lcfirst=首字母小写;ucfirst=首字母大写; 25 | 'letter_rule' => "lcfirst", 26 | // url前缀 27 | 'prefix'=>"", 28 | ], 29 | // (选配)是否自动注册路由 30 | 'auto_register_routes'=>false, 31 | // (必须)缓存配置 32 | 'cache' => [ 33 | // 是否开启缓存 34 | 'enable' => false, 35 | ], 36 | // (必须)权限认证配置 37 | 'auth' => [ 38 | // 是否启用密码验证 39 | 'enable' => false, 40 | // 全局访问密码 41 | 'password' => "123456", 42 | // 密码加密盐 43 | 'secret_key' => "apidoc#hg_code", 44 | // 授权访问后的有效期 45 | 'expire' => 24*60*60 46 | ], 47 | // 全局参数 48 | 'params'=>[ 49 | // (选配)全局的请求Header 50 | 'header'=>[ 51 | // name=字段名,type=字段类型,require=是否必须,default=默认值,desc=字段描述 52 | ['name'=>'Authorization','type'=>'string','require'=>true,'desc'=>'身份令牌Token'], 53 | ], 54 | // (选配)全局的请求Query 55 | 'query'=>[ 56 | // 同上 header 57 | ], 58 | // (选配)全局的请求Body 59 | 'body'=>[ 60 | // 同上 header 61 | ], 62 | ], 63 | // 全局响应体 64 | 'responses'=>[ 65 | // 成功响应体 66 | 'success'=>[ 67 | ['name'=>'code','desc'=>'业务代码','type'=>'int','require'=>1], 68 | ['name'=>'message','desc'=>'业务信息','type'=>'string','require'=>1], 69 | //参数同上 headers;main=true来指定接口Returned参数挂载节点 70 | ['name'=>'data','desc'=>'业务数据','main'=>true,'type'=>'object','require'=>1], 71 | ], 72 | // 异常响应体 73 | 'error'=>[ 74 | ['name'=>'code','desc'=>'业务代码','type'=>'int','require'=>1,'md'=>'/docs/HttpError.md'], 75 | ['name'=>'message','desc'=>'业务信息','type'=>'string','require'=>1], 76 | ] 77 | ], 78 | // (选配)全局响应状态码 79 | 'responses_status'=>[ 80 | [ 81 | 'name'=>'200', 82 | 'desc'=>'请求成功' 83 | ], 84 | [ 85 | 'name'=>'401', 86 | 'desc'=>'登录令牌无效', 87 | 'contentType'=>'' 88 | ], 89 | ], 90 | //(选配)默认作者 91 | 'default_author'=>'', 92 | //(选配)默认请求类型 93 | 'default_method'=>'GET', 94 | //(选配)Apidoc允许跨域访问 95 | 'allowCrossDomain'=>false, 96 | /** 97 | * (选配)解析时忽略带@注解的关键词,当注解中存在带@字符并且非Apidoc注解,如 @key test,此时Apidoc页面报类似以下错误时: 98 | * [Semantical Error] The annotation "@key" in method xxx() was never imported. Did you maybe forget to add a "use" statement for this annotation? 99 | */ 100 | 'ignored_annitation'=>[], 101 | 102 | // (选配)解析时忽略的方法 103 | 'ignored_methods'=>[], 104 | 105 | // (选配)数据库配置 106 | 'database'=>[], 107 | // (选配)Markdown文档 108 | 'docs' => [], 109 | // (选配)接口生成器配置 注意:是一个二维数组 110 | 'generator' =>[] 111 | ] 112 | ]; 113 | -------------------------------------------------------------------------------- /src/providers/ThinkPHPService.php: -------------------------------------------------------------------------------- 1 | initConfig(); 35 | 36 | $this->registerRoutes(function () use($config){ 37 | //注册apidoc所需路由 38 | self::registerApidocRoutes(function ($route)use ($config){ 39 | $registerRoute = Route::any($route['uri'], $route['callback']); 40 | $registerRoute->middleware([ThinkPHPMiddleware::class]); 41 | if (!empty($config['allowCrossDomain'])) { 42 | $registerRoute->allowCrossDomain(); 43 | } 44 | }); 45 | 46 | // 自动注册路由 47 | self::autoRegisterRoutes(function ($routeData){ 48 | $appRoute = $this->app->route; 49 | $appName = $this->app->http->getName(); 50 | foreach ($routeData as $controller) { 51 | $routeGroup = $appRoute->getGroup(); 52 | // 这里注册的控制器中间件会对所有的路由生效(包括apidoc自身的路由) 53 | // if (!empty($controller['middleware'])){ 54 | // $routeGroup->middleware($controller['middleware']); 55 | // } 56 | if (count($controller['methods'])){ 57 | foreach ($controller['methods'] as $method) { 58 | if (!empty($appName)){ 59 | $method['url'] = str_replace("/".$appName,'',$method['url']); 60 | } 61 | $apiMethods = Helper::handleApiMethod($method['method']); 62 | $apiMethods = implode("|",$apiMethods); 63 | $route = $routeGroup->addRule($method['url'],$method['controller']."@".$method['name'],$apiMethods); 64 | // 将控制器中间件注册到控制器下的所有路由下,避免影响全局路由 65 | if (!empty($controller['middleware'])){ 66 | $route->middleware($controller['middleware']); 67 | } 68 | if (!empty($method['middleware'])){ 69 | $route->middleware($method['middleware']); 70 | } 71 | } 72 | } 73 | } 74 | }); 75 | }); 76 | } 77 | 78 | static function registerRoute($route){ 79 | $registerRoute = Route::any($route['uri'], $route['callback']); 80 | } 81 | 82 | static function databaseQuery($sql){ 83 | return Db::query($sql); 84 | } 85 | 86 | static function getTablePrefix(){ 87 | $driver = config('database.default'); 88 | $table_prefix=config('database.connections.'.$driver.'.prefix'); 89 | return $table_prefix; 90 | } 91 | 92 | static function getRootPath() 93 | { 94 | return App::getRootPath(); 95 | } 96 | 97 | static function getRuntimePath() 98 | { 99 | return App::getRuntimePath(); 100 | } 101 | 102 | static function setLang($locale){ 103 | \think\facade\App::loadLangPack($locale); 104 | Lang::setLangSet($locale); 105 | } 106 | 107 | static function getLang($lang){ 108 | return Lang::get($lang); 109 | } 110 | 111 | static function handleResponseJson($res){ 112 | return json($res); 113 | } 114 | 115 | 116 | } -------------------------------------------------------------------------------- /src/utils/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'folder'=>'apidoc' 13 | ] 14 | ]; 15 | protected static $config = []; 16 | 17 | 18 | public static function get($field=""){ 19 | 20 | if (!empty(static::$config)) { 21 | $config = static::$config; 22 | }else{ 23 | throw new ErrorException('ConfigProvider get error'); 24 | } 25 | 26 | return Helper::getObjectFindByField($config,$field); 27 | } 28 | 29 | public static function set($config){ 30 | if (!(!empty($config['cache']) && !empty($config['cache']['folder']))){ 31 | if (!empty($config['cache'])){ 32 | $config['cache']['folder'] =static::$defaultConfig['cache']['folder']; 33 | } 34 | } 35 | $config = static::handleConfig($config); 36 | static::$config = $config; 37 | } 38 | 39 | public static function handleConfig($config){ 40 | if (!empty($config['params'])){ 41 | if (!empty($config['params']['header'])){ 42 | $config['params']['header'] = Helper::handleArrayParams($config['params']['header'],"desc",$config); 43 | } 44 | if (!empty($config['params']['query'])){ 45 | $config['params']['query'] = Helper::handleArrayParams($config['params']['query'],"desc",$config); 46 | } 47 | if (!empty($config['params']['body'])){ 48 | $config['params']['body'] = Helper::handleArrayParams($config['params']['body'],"desc",$config); 49 | } 50 | } 51 | if (!empty($config['responses'])){ 52 | if (!empty($config['responses']['success'])){ 53 | $config['responses']['success'] = Helper::handleArrayParams($config['responses']['success'],"desc",$config); 54 | } 55 | if (!empty($config['responses']['error'])){ 56 | $config['responses']['error'] = Helper::handleArrayParams($config['responses']['error'],"desc",$config); 57 | } 58 | } 59 | if (!empty($config['title'])){ 60 | $config['title'] = Lang::getLang($config['title'],$config); 61 | } 62 | return $config; 63 | } 64 | 65 | public static function getFeConfig($filterAppKeys=[]){ 66 | $config = static::$config; 67 | 68 | $feConfig = [ 69 | 'title' =>!empty($config['title'])?Lang::getLang($config['title'] ):'', 70 | 'desc' =>!empty($config['title'])?Lang::getLang($config['desc']):'', 71 | 'apps'=>!empty($config['apps'])?$config['apps']:[], 72 | 'cache'=>!empty($config['cache'])?$config['cache']:[], 73 | 'params'=>!empty($config['params'])?$config['params']:[], 74 | 'responses'=>!empty($config['responses'])?$config['responses']:[], 75 | 'generator'=>!empty($config['generator'])?$config['generator']:[], 76 | 'code_template'=>!empty($config['code_template'])?$config['code_template']:[], 77 | 'share'=>!empty($config['share'])?$config['share']:[], 78 | 'export_config'=>!empty($config['export_config'])?$config['export_config']:[], 79 | ]; 80 | if (!empty($feConfig['apps']) && count($feConfig['apps'])){ 81 | // 清除apps配置中的password 82 | $feConfig['apps'] = Helper::handleAppsConfig($feConfig['apps'],true,"","",$filterAppKeys); 83 | } 84 | 85 | if (!empty($feConfig['generator'])){ 86 | $generatorList = []; 87 | $generators= Helper::handleArrayParams($feConfig['generator'],"title"); 88 | foreach ($generators as $item) { 89 | if (isset($item['enable']) && $item['enable'] === false){ 90 | continue; 91 | } 92 | if (!empty($item['form']) && !empty($item['form']['items']) && count($item['form']['items'])){ 93 | $item['form']['items'] = Helper::handleArrayParams( $item['form']['items'],"title"); 94 | } 95 | $generatorList[]=$item; 96 | } 97 | $feConfig['generator'] = $generatorList; 98 | } 99 | return $feConfig; 100 | } 101 | 102 | 103 | } -------------------------------------------------------------------------------- /src/parses/ParseMarkdown.php: -------------------------------------------------------------------------------- 1 | config = $config; 18 | } 19 | 20 | /** 21 | * 获取md文档菜单 22 | * @return array 23 | */ 24 | public function getDocsMenu($appKey,string $lang): array 25 | { 26 | $config = $this->config; 27 | $docData = []; 28 | if (!empty($config['docs']) && count($config['docs']) > 0) { 29 | $docData = $this->handleDocsMenuData($config['docs'],$appKey,$lang); 30 | } 31 | return $docData; 32 | } 33 | 34 | /** 35 | * 处理md文档菜单数据 36 | * @param array $menus 37 | * @return array 38 | */ 39 | protected function handleDocsMenuData(array $menus,$appKey,string $lang): array 40 | { 41 | $list = []; 42 | foreach ($menus as $item) { 43 | $item['title'] = Lang::getLang($item['title']); 44 | if (!empty($item['appKey']) && $item['appKey'] != $appKey){ 45 | continue; 46 | } 47 | 48 | if (!empty($item['children']) && count($item['children']) > 0) { 49 | $item['children'] = $this->handleDocsMenuData($item['children'],$appKey,$lang); 50 | $item['menuKey'] = Helper::createRandKey("md_group"); 51 | } else { 52 | $filePath = static::getFilePath($appKey,$item['path'],$lang); 53 | if (!file_exists($filePath['filePath'])) { 54 | continue; 55 | } 56 | 57 | if(!empty($item['path'])){ 58 | $item['path'] = Helper::replaceTemplate($item['path'],['lang'=>$lang]); 59 | } 60 | $item['type'] = 'md'; 61 | $item['menuKey'] = Helper::createApiKey($item['path']); 62 | } 63 | $list[] = $item; 64 | } 65 | return $list; 66 | } 67 | 68 | public static function getFilePath(string $appKey, string $path,$lang=""){ 69 | if (!empty($appKey)){ 70 | $currentAppConfig = Helper::getCurrentAppConfig($appKey); 71 | $currentApps = $currentAppConfig['apps']; 72 | $fullPath = Helper::replaceCurrentAppTemplate($path, $currentApps); 73 | }else{ 74 | $fullPath = $path; 75 | } 76 | $fullPath = Helper::replaceTemplate($fullPath,[ 77 | 'lang'=>$lang 78 | ]); 79 | 80 | if (strpos($fullPath, '#') !== false) { 81 | $mdPathArr = explode("#", $fullPath); 82 | $mdPath=$mdPathArr[0]; 83 | $mdAnchor =$mdPathArr[1]; 84 | } else { 85 | $mdPath = $fullPath; 86 | $mdAnchor=""; 87 | } 88 | $fileSuffix = ""; 89 | if (strpos($fullPath, '.md') === false) { 90 | $fileSuffix = ".md"; 91 | } 92 | $filePath = APIDOC_ROOT_PATH . $mdPath . $fileSuffix; 93 | return [ 94 | 'filePath'=>$filePath, 95 | 'anchor'=>$mdAnchor 96 | ]; 97 | } 98 | 99 | 100 | /** 101 | * 获取md文档内容 102 | * @param string $appKey 103 | * @param string $path 104 | * @return string 105 | */ 106 | public static function getContent(string $appKey, string $path,$lang="") 107 | { 108 | $filePathArr = static::getFilePath($appKey,$path,$lang); 109 | $mdAnchor = $filePathArr['anchor']; 110 | $filePath = $filePathArr['filePath']; 111 | if (!file_exists($filePath)) { 112 | return $path; 113 | } 114 | $contents = DirAndFile::getFileContent($filePath); 115 | // 获取指定h2标签内容 116 | if (!empty($mdAnchor)){ 117 | if (strpos($contents, '## ') !== false) { 118 | $contentArr = explode("\r\n", $contents); 119 | $contentText = ""; 120 | foreach ($contentArr as $line){ 121 | $contentText.="\r\n".trim($line); 122 | } 123 | $contentArr = explode("\r\n## ", $contentText); 124 | $content=""; 125 | foreach ($contentArr as $item){ 126 | $itemArr = explode("\r\n", $item); 127 | if (!empty($itemArr) && $itemArr[0] && $mdAnchor===$itemArr[0]){ 128 | $content = str_replace($itemArr[0]."\r\n", '', $item); 129 | break; 130 | } 131 | } 132 | return $content; 133 | } 134 | } 135 | return $contents; 136 | } 137 | 138 | 139 | } -------------------------------------------------------------------------------- /src/utils/AutoRegisterRouts.php: -------------------------------------------------------------------------------- 1 | config = $config; 29 | } 30 | 31 | /** 32 | * 解析所有应用的api 33 | * @return array 34 | */ 35 | public function getAppsApis(){ 36 | $apps = Helper::getAllApps($this->config['apps']); 37 | $apiList = []; 38 | if (!empty($apps) && count($apps)){ 39 | foreach ($apps as $app) { 40 | $apis = $this->getAppApis($app); 41 | $apiList=array_merge($apiList,$apis); 42 | } 43 | } 44 | return $apiList; 45 | 46 | } 47 | 48 | /** 49 | * 生成api接口数据 50 | * @param array $app 51 | * @return array 52 | */ 53 | public function getAppApis($app) 54 | { 55 | $controllers = []; 56 | if (!empty($app['controllers']) && count($app['controllers']) > 0) { 57 | // 配置的控制器列表 58 | $controllers = (new ParseApiMenus($this->config))->getConfigControllers($app['path'],$app['controllers']); 59 | }else if(!empty($app['path']) && is_array($app['path']) && count($app['path'])){ 60 | $parseApiMenus = new ParseApiMenus($this->config); 61 | foreach ($app['path'] as $path) { 62 | $controllersList = $parseApiMenus->getDirControllers($path); 63 | $controllers = array_merge($controllers,$controllersList); 64 | } 65 | } else if(!empty($app['path']) && is_string($app['path'])) { 66 | // 默认读取所有的 67 | $controllers = (new ParseApiMenus($this->config))->getDirControllers($app['path']); 68 | } 69 | 70 | $routeData = []; 71 | if (!empty($controllers) && count($controllers) > 0) { 72 | foreach ($controllers as $class) { 73 | $classData = $this->parseController($class); 74 | if ($classData !== false) { 75 | $routeData[] = $classData; 76 | } 77 | } 78 | } 79 | return $routeData; 80 | } 81 | 82 | 83 | public function parseController($class) 84 | { 85 | $refClass = new ReflectionClass($class); 86 | $classTextAnnotations = ParseAnnotation::parseTextAnnotation($refClass); 87 | $classAnnotations = (new ParseAnnotation($this->config))->getClassAnnotation($refClass); 88 | if (in_array("NotParse", $classTextAnnotations) || isset($classAnnotations['notParse'])) { 89 | return false; 90 | } 91 | 92 | 93 | $methodList = []; 94 | foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $refMethod) { 95 | $methodItem = $this->parseApiMethod($refClass,$refMethod); 96 | if ($methodItem===false){ 97 | continue; 98 | } 99 | $methodList[] = $methodItem; 100 | } 101 | if (count($methodList)===0){ 102 | return false; 103 | } 104 | $data = [ 105 | 'name'=>$refClass->name, 106 | 'methods'=>$methodList, 107 | ]; 108 | 109 | //控制器中间件 110 | if (!empty($classAnnotations['routeMiddleware']) && !empty($classAnnotations['routeMiddleware'])) { 111 | $data['middleware'] = $classAnnotations['routeMiddleware']; 112 | } 113 | return $data; 114 | } 115 | 116 | 117 | protected function parseApiMethod($refClass,$refMethod){ 118 | if (empty($refMethod->name) || in_array($refMethod->name,$this->filterMethods)) { 119 | return false; 120 | } 121 | $config = $this->config; 122 | $textAnnotations = ParseAnnotation::parseTextAnnotation($refMethod); 123 | $methodAnnotation = (new ParseAnnotation($config))->getMethodAnnotation($refMethod); 124 | if (in_array("NotParse", $textAnnotations) || isset($methodAnnotation['notParse'])) { 125 | return false; 126 | } 127 | 128 | if (empty($methodAnnotation['method'])) { 129 | $method = !empty($config['default_method']) ? strtoupper($config['default_method']) : '*'; 130 | }else{ 131 | $method = $methodAnnotation['method']; 132 | } 133 | if (empty($methodAnnotation['url'])) { 134 | $url = ParseApiDetail::autoCreateUrl($refClass->name,$refMethod->name,$config); 135 | }else{ 136 | $url = $methodAnnotation['url']; 137 | } 138 | if (!empty($url) && substr($url, 0, 1) != "/") { 139 | $url = "/" . $url; 140 | } 141 | $data = [ 142 | 'url'=>$url, 143 | 'method'=>$method, 144 | 'name'=>$refMethod->name, 145 | 'controller'=>$refClass->name, 146 | ]; 147 | if (!empty($methodAnnotation['routeMiddleware']) && !empty($methodAnnotation['routeMiddleware'])) { 148 | $data['middleware'] = $methodAnnotation['routeMiddleware']; 149 | } 150 | return $data; 151 | 152 | } 153 | 154 | } -------------------------------------------------------------------------------- /src/providers/BaseService.php: -------------------------------------------------------------------------------- 1 | 'config','route'=>'getConfig'], 15 | ['rule'=>'apiMenus','route'=>'getApiMenus'], 16 | ['rule'=>'apiDetail','route'=>'getApiDetail'], 17 | ['rule'=>'docMenus','route'=>'getMdMenus'], 18 | ['rule'=>'docDetail','route'=>'getMdDetail'], 19 | ['rule'=>'verifyAuth','route'=>'verifyAuth'], 20 | ['rule'=>'generator','route'=>'createGenerator'], 21 | ['rule'=>'cancelAllCache','route'=>'cancelAllCache'], 22 | ['rule'=>'createAllCache','route'=>'createAllCache'], 23 | ['rule'=>'renderCodeTemplate','route'=>'renderCodeTemplate'], 24 | ['rule'=>'allApiMenus','route'=>'getAllApiMenus'], 25 | ['rule'=>'addApiShare','route'=>'addApiShare'], 26 | ['rule'=>'getApiShareList','route'=>'getApiShareList'], 27 | ['rule'=>'getApiShareDetail','route'=>'getApiShareDetail'], 28 | ['rule'=>'deleteApiShare','route'=>'deleteApiShare'], 29 | ['rule'=>'handleApiShareAction','route'=>'handleApiShareAction'], 30 | ['rule'=>'exportSwagger','route'=>'exportSwagger'], 31 | ]; 32 | 33 | 34 | 35 | /** 36 | * 获取apidoc配置 37 | * @return array 返回apidoc配置 38 | */ 39 | abstract static function getApidocConfig(); 40 | 41 | 42 | /** 43 | * 注册apidoc路由 44 | * @param $route 路由参数 45 | * @return mixed 46 | */ 47 | abstract static function registerRoute($route); 48 | 49 | /** 50 | * 执行Sql语句 51 | * @return mixed 52 | */ 53 | abstract static function databaseQuery($sql); 54 | 55 | /** 56 | * 获取项目根目录 57 | * @return string 返回项目根目录 58 | */ 59 | abstract static function getRootPath(); 60 | 61 | /** 62 | * 获取缓存目录 63 | * @return string 返回项目缓存目录 64 | */ 65 | abstract static function getRuntimePath(); 66 | 67 | 68 | /** 69 | * 设置当前语言 70 | * @param $locale 语言标识 71 | * @return mixed 72 | */ 73 | abstract static function setLang($locale); 74 | 75 | /** 76 | * 获取语言定义 77 | * @param $lang 78 | * @return string 79 | */ 80 | abstract static function getLang($lang); 81 | 82 | 83 | /** 84 | * 处理apidoc接口响应的数据 85 | * @return mixed 86 | */ 87 | abstract static function handleResponseJson($res); 88 | 89 | abstract static function getTablePrefix(); 90 | 91 | // 自动注册api路由 92 | static public function autoRegisterRoutes($routeFun,$config=""){ 93 | if (empty($config)){ 94 | $config = self::getApidocConfig(); 95 | } 96 | if (isset($config['auto_register_routes']) && $config['auto_register_routes']===true) { 97 | $cacheKey = "apis/autoRegisterRoutes"; 98 | if (!empty($config['cache']) && $config['cache']['enable']) { 99 | $cacheData = (new Cache())->get($cacheKey); 100 | if (!empty($cacheData)) { 101 | $autoRegisterApis = $cacheData; 102 | } else { 103 | $autoRegisterApis = (new AutoRegisterRouts($config))->getAppsApis(); 104 | (new Cache())->set($cacheKey, $autoRegisterApis); 105 | } 106 | } else { 107 | $autoRegisterApis = (new AutoRegisterRouts($config))->getAppsApis(); 108 | } 109 | $routeFun($autoRegisterApis); 110 | } 111 | } 112 | 113 | public function initConfig(){ 114 | ! defined('APIDOC_ROOT_PATH') && define('APIDOC_ROOT_PATH', $this->getRootPath()); 115 | ! defined('APIDOC_STORAGE_PATH') && define('APIDOC_STORAGE_PATH', $this->getRuntimePath()); 116 | $config = self::getApidocConfig(); 117 | if (empty($config['database_query_function'])){ 118 | $config['database_query_function'] = function ($sql){ 119 | return self::databaseQuery($sql); 120 | }; 121 | } 122 | if (empty($config['lang_register_function'])){ 123 | $config['lang_register_function'] = function ($sql){ 124 | return self::setLang($sql); 125 | }; 126 | } 127 | if (empty($config['lang_get_function'])){ 128 | $config['lang_get_function'] = function ($lang){ 129 | return self::getLang($lang); 130 | }; 131 | } 132 | $config['handle_response_json'] = function ($res){ 133 | return self::handleResponseJson($res); 134 | }; 135 | $table_prefix = self::getTablePrefix(); 136 | if (!empty($config['database'])){ 137 | if (empty($config['prefix'])){ 138 | $config['database']['prefix'] = $table_prefix; 139 | } 140 | }else{ 141 | $config['database']=[ 142 | 'prefix'=>$table_prefix 143 | ]; 144 | } 145 | ConfigProvider::set($config); 146 | } 147 | 148 | /** 149 | * @param null $routeFun 150 | */ 151 | static public function registerApidocRoutes($routeFun=null){ 152 | $routes = static::$routes; 153 | $controller_namespace = '\hg\apidoc\Controller@'; 154 | $route_prefix = "/apidoc"; 155 | $config = self::getApidocConfig(); 156 | if (!empty($config) && !empty($config['route_prefix'])){ 157 | $route_prefix = $config['route_prefix']; 158 | } 159 | foreach ($routes as $item) { 160 | $route = [ 161 | 'uri'=>$route_prefix."/".$item['rule'], 162 | 'callback'=>$controller_namespace.$item['route'], 163 | 'route'=>$item['route'], 164 | ]; 165 | if (!empty($routeFun)){ 166 | $routeFun($route); 167 | }else{ 168 | self::registerRoute($route); 169 | } 170 | } 171 | } 172 | 173 | } -------------------------------------------------------------------------------- /src/utils/ApiShare.php: -------------------------------------------------------------------------------- 1 | $params['name'], 29 | 'type' => $params['type'], 30 | ]; 31 | if (!empty($params['appKeys'])) { 32 | $data['appKeys'] = $params['appKeys']; 33 | } 34 | if (!empty($params['apiKeys'])) { 35 | $data['apiKeys'] = $params['apiKeys']; 36 | } 37 | if (!empty($params['password'])) { 38 | $data['password'] = $params['password']; 39 | } 40 | $data['create_at'] = date('Y-m-d h:i:s'); 41 | $data['create_time'] = time(); 42 | $res = (new Cache())->set($cacheKey, $data); 43 | return $res; 44 | } 45 | 46 | public function getSharePageList($config,$pageIndex,$pageSize){ 47 | $path = APIDOC_STORAGE_PATH . $config['cache']['folder'] . "/share"; 48 | 49 | $list = DirAndFile::getFileList($path); 50 | $data = []; 51 | $cache = new Cache(); 52 | 53 | foreach ($list as $item) { 54 | $fileNameArr = explode("_", $item['name']); 55 | $cacheKey = "share/" . $fileNameArr[0] . "_" . $fileNameArr[1]; 56 | $cacheData = $cache->get($cacheKey); 57 | $itemData = [ 58 | 'key' => $fileNameArr[1], 59 | 'name' => $cacheData['name'], 60 | 'type' => $cacheData['type'], 61 | 'create_time' => $cacheData['create_time'], 62 | 'create_at' => $cacheData['create_at'], 63 | ]; 64 | $data[] = $itemData; 65 | } 66 | $data = Helper::arraySortByKey($data, 'create_time', SORT_DESC); 67 | $page = $pageIndex-1; 68 | $res = array_slice($data,$page*$pageSize,$pageSize); 69 | return [ 70 | 'total'=>count($data), 71 | 'data'=>$res 72 | ]; 73 | } 74 | 75 | 76 | public function getShareDetailByKey($shareKey) 77 | { 78 | if (empty($shareKey)) { 79 | throw new ErrorException('field not found', ['field' => 'shareKey']); 80 | } 81 | $cache = new Cache(); 82 | $cacheKey = static::getShareCacheKey($shareKey); 83 | $cacheData = $cache->get($cacheKey); 84 | if (empty($cacheData)) { 85 | throw new ErrorException("share not exists"); 86 | } 87 | return $cacheData; 88 | } 89 | 90 | public function checkShareAuth($config, $params) 91 | { 92 | if (empty($params['shareKey'])) { 93 | throw new ErrorException('field not found', ['field' => 'shareKey']); 94 | } 95 | $cacheData = $this->getShareDetailByKey($params['shareKey']); 96 | 97 | if (!empty($cacheData['password'])) { 98 | //验证密码 99 | if (empty($params['token'])) { 100 | throw new ErrorException("token not found"); 101 | } 102 | $password = md5($cacheData['password']); 103 | $cacheData['pass'] = $password; 104 | $checkAuth = (new Auth($config))->checkToken($params['token'], $password); 105 | if (!$checkAuth) { 106 | throw new ErrorException("token error"); 107 | } 108 | } 109 | return $cacheData; 110 | } 111 | 112 | public static function getAppShareApis(array $config, array $apps, $parentKey = "", $filterAppKeys = [], $isParseDetail = false) 113 | { 114 | $appList = []; 115 | $separ = !empty($parentKey) ? ',' : ''; 116 | foreach ($apps as $app) { 117 | $appKey = $parentKey . $separ . $app['key']; 118 | if (!empty($app['items']) && count($app['items'])) { 119 | $items = static::getAppShareApis($config, $app['items'], $appKey, $filterAppKeys, $isParseDetail); 120 | $app['children'] = $items; 121 | } else { 122 | $app['appKey'] = $appKey; 123 | $apiData = (new ParseApiMenus($config))->renderApiMenus($appKey, $isParseDetail); 124 | $app['children'] = $apiData['data']; 125 | } 126 | if (!empty($filterAppKeys) && count($filterAppKeys) && !in_array($appKey, $filterAppKeys) && empty($app['items'])) { 127 | continue; 128 | } 129 | $app['menuKey'] = $appKey; 130 | $appList[] = $app; 131 | } 132 | 133 | return $appList; 134 | } 135 | 136 | public function getShareData($config, $key) 137 | { 138 | $shareData = $this->getShareDetailByKey($key); 139 | $filterAppKeys = !empty($shareData['appKeys']) ? $shareData['appKeys'] : []; 140 | $configApps = Helper::handleAppsConfig($config['apps'], false, $config); 141 | $appList = static::getAppShareApis($config, $configApps, "", $filterAppKeys, true); 142 | if ($shareData['type'] == 'api') { 143 | $appList = Helper::filterTreeNodesByKeys($appList, $shareData['apiKeys'], 'menuKey'); 144 | } 145 | return [ 146 | 'shareData' => $shareData, 147 | 'apiData' => $appList, 148 | ]; 149 | } 150 | 151 | public function handleApiShareAction($config, $key, $index) 152 | { 153 | 154 | $actionConfig = $config['share']['actions'][$index]; 155 | if (!empty($actionConfig['click'])) { 156 | $data = $this->getShareData($config, $key); 157 | $res = $actionConfig['click']($data['shareData'], $data['apiData']); 158 | return $res; 159 | } 160 | return false; 161 | 162 | } 163 | 164 | 165 | } -------------------------------------------------------------------------------- /src/parses/ParseModel.php: -------------------------------------------------------------------------------- 1 | config = $config; 22 | } 23 | 24 | public function parseModelTable($model, $classReflect, $methodName = "") 25 | { 26 | if (!is_callable(array($model, 'getTable'))) { 27 | return false; 28 | } 29 | $config = $this->config; 30 | try { 31 | // 获取所有模型属性 32 | $propertys = $classReflect->getDefaultProperties(); 33 | $tableName = $model->getTable(); 34 | $configTablePrefix = !empty($config['database']) && !empty($config['database']['prefix']) ? $config['database']['prefix'] : ""; 35 | if (!empty($configTablePrefix) && strpos($tableName, $configTablePrefix) === false) { 36 | $tableName = $configTablePrefix . $model->getTable(); 37 | } 38 | $table = $this->getTableDocument($tableName, $propertys, $model); 39 | if (empty($methodName)) { 40 | return $table; 41 | } 42 | 43 | $methodReflect = $classReflect->getMethod($methodName); 44 | $annotations = (new ParseAnnotation($config))->getMethodAnnotation($methodReflect); 45 | if (!empty($annotations['field'])) { 46 | $table = ParseApiDetail::filterParamsField($table, $annotations['field'], 'field'); 47 | } 48 | if (!empty($annotations['withoutField'])) { 49 | $table = ParseApiDetail::filterParamsField($table, $annotations['withoutField'], 'withoutField'); 50 | } 51 | if (!empty($annotations['addField'])) { 52 | $addFieldData = []; 53 | if (is_int(Helper::arrayKeyFirst($annotations['addField']))) { 54 | $addFieldData = $annotations['addField']; 55 | } else { 56 | $addFieldData = [$annotations['addField']]; 57 | } 58 | $addFieldList = []; 59 | $parseApiDetail = new ParseApiDetail($config); 60 | $field = 'param'; 61 | foreach ($addFieldData as $fieldItem) { 62 | if (!empty($fieldItem['ref'])) { 63 | $refParams = $parseApiDetail->renderRef($fieldItem['ref'], $field); 64 | if (!empty($refParams[$field])) { 65 | $fieldItem = $parseApiDetail->handleRefData($fieldItem, $refParams[$field], $field); 66 | } 67 | } 68 | if (!empty($fieldItem['md'])) { 69 | $fieldItem['md'] = ParseMarkdown::getContent("", $fieldItem['md']); 70 | } 71 | // 自定义解析 72 | if (!empty($config['parsesAnnotation'])) { 73 | $callback = $config['parsesAnnotation']($fieldItem); 74 | if (!empty($callback)) { 75 | $fieldItem = $callback; 76 | } 77 | } 78 | $addFieldList[] = $fieldItem; 79 | } 80 | $table = Helper::arrayMergeAndUnique("name", $table, $addFieldList); 81 | } 82 | return $table; 83 | } catch (\ReflectionException $e) { 84 | throw new ErrorException('Class ' . get_class($model) . ' ' . $e->getMessage()); 85 | } 86 | 87 | } 88 | 89 | /** 90 | * 获取模型实例 91 | * @param $method 92 | * @return mixed|null 93 | */ 94 | public static function getModelClass($namespaceName) 95 | { 96 | if (!empty($namespaceName) && class_exists($namespaceName)) { 97 | $modelInstance = new $namespaceName(); 98 | if (is_callable(array($modelInstance, 'getTable'))) { 99 | return $modelInstance; 100 | } 101 | } 102 | return null; 103 | } 104 | 105 | 106 | /** 107 | * 获取模型注解数据 108 | * @param $tableName 109 | * @param $propertys 110 | * @return array 111 | */ 112 | public function getTableDocument($tableName, array $propertys, $model = null): array 113 | { 114 | $config = $this->config; 115 | $fieldComment = []; 116 | if (empty($config['database_query_function'])) { 117 | throw new ErrorException("not datatable_query_function config"); 118 | } 119 | $tableColumns = $config['database_query_function']("SHOW FULL COLUMNS FROM `" . $tableName . "`"); 120 | foreach ($tableColumns as $columns) { 121 | $columns = Helper::objectToArray($columns); 122 | $name = $columns['Field']; 123 | $desc = $columns['Comment']; 124 | $mock = ""; 125 | $md = ""; 126 | if (isset($propertys['convertNameToCamel']) && $propertys['convertNameToCamel'] === true) { 127 | $name = Helper::camel($name); 128 | } 129 | if (!empty($desc)) { 130 | // 存在字段注释 131 | $desc = Lang::getLang($desc); 132 | if (strpos($desc, 'mock(') !== false) { 133 | // 存在mock 134 | preg_match('#mock\((.*)\)#s', $desc, $mocks); 135 | if (!empty($mocks[1])) { 136 | $mock = $mocks[1]; 137 | $desc = str_replace($mocks[0], "", $desc); 138 | } 139 | } 140 | if (strpos($desc, 'md="') !== false) { 141 | // 存在md 142 | preg_match('#md="(.*)"#s', $desc, $mdRefs); 143 | if (!empty($mdRefs[1])) { 144 | $md = ParseMarkdown::getContent("", $mdRefs[1]); 145 | $desc = str_replace($mdRefs[0], "", $desc); 146 | } 147 | } 148 | } 149 | $fieldComment[] = [ 150 | "name" => $name, 151 | "type" => $columns['Type'], 152 | "desc" => $desc, 153 | "default" => $columns['Default'], 154 | "require" => $columns['Null'] != "YES", 155 | "mock" => $mock, 156 | "md" => $md, 157 | ]; 158 | } 159 | return $fieldComment; 160 | } 161 | 162 | } -------------------------------------------------------------------------------- /src/Auth.php: -------------------------------------------------------------------------------- 1 | authConfig = $authConfig; 24 | } 25 | 26 | /** 27 | * 验证密码是否正确 28 | * @param $password 29 | * @return false|string 30 | */ 31 | public function verifyAuth(string $password, string $appKey) 32 | { 33 | $authConfig = $this->authConfig; 34 | if (!empty($appKey)) { 35 | $currentAppConfig = Helper::getCurrentAppConfig($appKey); 36 | $currentApp = $currentAppConfig['appConfig']; 37 | if (!empty($currentApp) && !empty($currentApp['password'])) { 38 | // 应用密码 39 | if (md5($currentApp['password']) === $password) { 40 | return $this->createToken($currentApp['password'],$authConfig['expire']); 41 | } 42 | throw new ErrorException("password error"); 43 | } 44 | } 45 | if ($authConfig['enable']) { 46 | // 密码验证 47 | if (md5($authConfig['password']) === $password) { 48 | return $this->createToken($authConfig['password'],$authConfig['expire']); 49 | } 50 | throw new ErrorException("password error"); 51 | } 52 | return false; 53 | } 54 | 55 | /** 56 | * 验证token是否可用 57 | * @param $request 58 | * @return bool 59 | */ 60 | public function checkAuth($params=[]): bool 61 | { 62 | $authConfig = $this->authConfig; 63 | $token = !empty($params['token'])?$params['token']:""; 64 | $appKey = !empty($params['appKey'])?$params['appKey']:""; 65 | if (!empty($appKey)) { 66 | $currentAppConfig = Helper::getCurrentAppConfig($appKey); 67 | $currentApp = $currentAppConfig['appConfig']; 68 | if (!empty($currentApp) && !empty($currentApp['password'])) { 69 | if (empty($token)) { 70 | throw new ErrorException("token not found"); 71 | } 72 | // 应用密码 73 | if ($this->checkToken($token, $currentApp['password'])) { 74 | return true; 75 | } else { 76 | throw new ErrorException("token error"); 77 | } 78 | } else if (empty($authConfig['enable'])) { 79 | return true; 80 | } 81 | } 82 | if($authConfig['enable'] && empty($token)){ 83 | throw new ErrorException("token not found"); 84 | }else if (!empty($token) && !$this->checkToken($token, "") && $authConfig['enable']) { 85 | throw new ErrorException("token error"); 86 | } 87 | return true; 88 | } 89 | 90 | 91 | /** 92 | * 获取tokencode 93 | * @param string $password 94 | * @return string 95 | */ 96 | protected static function getTokenCode(string $password): string 97 | { 98 | return md5(md5($password)); 99 | } 100 | 101 | 102 | /** 103 | * 创建token 104 | * @param string $password 105 | * @return string 106 | */ 107 | public function createToken(string $password): string 108 | { 109 | $authConfig = $this->authConfig; 110 | $data = [ 111 | 'key'=>static::getTokenCode($password), 112 | 'expire'=>time()+$authConfig['expire'] 113 | ]; 114 | $code = json_encode($data); 115 | return static::handleToken($code, "CE",$authConfig['secret_key']); 116 | } 117 | 118 | /** 119 | * 验证token是否可用 120 | * @param $token 121 | * @return bool 122 | */ 123 | public function checkToken(string $token, string $password): bool 124 | { 125 | $authConfig = $this->authConfig; 126 | if (empty($password)){ 127 | $password = $authConfig['password']; 128 | } 129 | $decode = static::handleToken($token, "DE",$authConfig['secret_key']); 130 | $deData = json_decode($decode,true); 131 | 132 | if (!empty($deData['key']) && $deData['key'] === static::getTokenCode($password) && !empty($deData['expire']) && $deData['expire']>time()){ 133 | return true; 134 | } 135 | 136 | return false; 137 | } 138 | 139 | 140 | 141 | /** 142 | * 处理token 143 | * @param $string 144 | * @param string $operation 145 | * @param string $key 146 | * @param int $expiry 147 | * @return false|string 148 | */ 149 | protected static function handleToken(string $string, string $operation = 'DE', string $key = '', int $expiry = 0):string 150 | { 151 | $ckey_length = 4; 152 | $key = md5($key); 153 | $keya = md5(substr($key, 0, 16)); 154 | $keyb = md5(substr($key, 16, 16)); 155 | $keyc = $ckey_length ? ($operation == 'DE' ? substr($string, 0, $ckey_length) : substr(md5(microtime()), -$ckey_length)) : ''; 156 | $cryptkey = $keya . md5($keya . $keyc); 157 | $key_length = strlen($cryptkey); 158 | $string = $operation == 'DE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string; 159 | $string_length = strlen($string); 160 | $result = ''; 161 | $box = range(0, 255); 162 | $rndkey = array(); 163 | for ($i = 0; $i <= 255; $i++) { 164 | $rndkey[$i] = ord($cryptkey[$i % $key_length]); 165 | } 166 | for ($j = $i = 0; $i < 256; $i++) { 167 | $j = ($j + $box[$i] + $rndkey[$i]) % 256; 168 | $tmp = $box[$i]; 169 | $box[$i] = $box[$j]; 170 | $box[$j] = $tmp; 171 | } 172 | for ($a = $j = $i = 0; $i < $string_length; $i++) { 173 | $a = ($a + 1) % 256; 174 | $j = ($j + $box[$a]) % 256; 175 | $tmp = $box[$a]; 176 | $box[$a] = $box[$j]; 177 | $box[$j] = $tmp; 178 | $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); 179 | } 180 | if ($operation == 'DE') { 181 | $subNumber = (int)substr($result, 0, 10); 182 | if (($subNumber == 0 || $subNumber - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)) { 183 | return substr($result, 26); 184 | } else { 185 | return ''; 186 | } 187 | } else { 188 | return $keyc . str_replace('=', '', base64_encode($result)); 189 | } 190 | } 191 | 192 | } -------------------------------------------------------------------------------- /src/utils/Cache.php: -------------------------------------------------------------------------------- 1 | 0, 32 | 'cache_subdir' => true, 33 | 'prefix' => '', 34 | 'path' => '', 35 | 'hash_type' => 'md5', 36 | 'data_compress' => false, 37 | 'serialize' => [], 38 | ]; 39 | 40 | /** 41 | * 架构函数 42 | * @param array $options 参数 43 | */ 44 | public function __construct( array $options = []) 45 | { 46 | if (!empty($options)) { 47 | $this->options = array_merge($this->options, $options); 48 | } 49 | 50 | if (empty($this->options['path'])) { 51 | $this->options['path'] = APIDOC_STORAGE_PATH .'/'. 'apidoc'; 52 | } 53 | 54 | if (substr($this->options['path'], -1) != DIRECTORY_SEPARATOR) { 55 | $this->options['path'] .= DIRECTORY_SEPARATOR; 56 | } 57 | } 58 | 59 | /** 60 | * 取得变量的存储文件名 61 | * @access public 62 | * @param string $name 缓存变量名 63 | * @return string 64 | */ 65 | public function getCacheKey(string $name): string 66 | { 67 | $name = $name."_".hash($this->options['hash_type'], $name); 68 | 69 | if ($this->options['prefix']) { 70 | $name = $this->options['prefix'] . DIRECTORY_SEPARATOR . $name; 71 | } 72 | 73 | return $this->options['path'] . $name . '.php'; 74 | } 75 | 76 | /** 77 | * 序列化数据 78 | * @access protected 79 | * @param mixed $data 缓存数据 80 | * @return string 81 | */ 82 | protected function serialize($data): string 83 | { 84 | if (is_numeric($data)) { 85 | return (string) $data; 86 | } 87 | 88 | $serialize = $this->options['serialize'][0] ?? "serialize"; 89 | 90 | return $serialize($data); 91 | } 92 | 93 | 94 | /** 95 | * 反序列化数据 96 | * @access protected 97 | * @param string $data 缓存数据 98 | * @return mixed 99 | */ 100 | protected function unserialize($data) 101 | { 102 | if (is_numeric($data)) { 103 | return $data; 104 | } 105 | 106 | $unserialize = $this->options['serialize'][1] ?? "unserialize"; 107 | 108 | return $unserialize($data); 109 | } 110 | 111 | 112 | /** 113 | * 获取有效期 114 | * @access protected 115 | * @param integer|DateTimeInterface|DateInterval $expire 有效期 116 | * @return int 117 | */ 118 | protected function getExpireTime($expire): int 119 | { 120 | if ($expire instanceof DateTimeInterface) { 121 | $expire = $expire->getTimestamp() - time(); 122 | } elseif ($expire instanceof DateInterval) { 123 | $expire = DateTime::createFromFormat('U', (string) time()) 124 | ->add($expire) 125 | ->format('U') - time(); 126 | } 127 | 128 | return (int) $expire; 129 | } 130 | 131 | /** 132 | * 获取缓存数据 133 | * @param string $name 缓存标识名 134 | * @return array|null 135 | */ 136 | protected function getRaw(string $name) 137 | { 138 | $filename = $this->getCacheKey($name); 139 | 140 | if (!is_file($filename)) { 141 | return; 142 | } 143 | 144 | $content = @file_get_contents($filename); 145 | 146 | if (false !== $content) { 147 | $expire = (int) substr($content, 8, 12); 148 | $createTime = filemtime($filename); 149 | if (0 != $expire && time() - $expire > $createTime) { 150 | //缓存过期删除缓存文件 151 | DirAndFile::unlink($item->getPathname()); 152 | return; 153 | } 154 | 155 | $content = substr($content, 32); 156 | 157 | if ($this->options['data_compress'] && function_exists('gzcompress')) { 158 | //启用数据压缩 159 | $content = gzuncompress($content); 160 | } 161 | 162 | return is_string($content) ? ['content' => $content, 'expire' => $expire,'create_time'=>$createTime] : null; 163 | } 164 | } 165 | 166 | /** 167 | * 判断缓存是否存在 168 | * @access public 169 | * @param string $name 缓存变量名 170 | * @return bool 171 | */ 172 | public function has($name): bool 173 | { 174 | return $this->getRaw($name) !== null; 175 | } 176 | 177 | /** 178 | * 读取缓存 179 | * @access public 180 | * @param string $name 缓存变量名 181 | * @param mixed $default 默认值 182 | * @return mixed 183 | */ 184 | public function get($name, $default = null) 185 | { 186 | $this->readTimes++; 187 | 188 | $raw = $this->getRaw($name); 189 | 190 | return is_null($raw) ? $default : $this->unserialize($raw['content']); 191 | } 192 | 193 | /** 194 | * 写入缓存 195 | * @access public 196 | * @param string $name 缓存变量名 197 | * @param mixed $value 存储数据 198 | * @param int|\DateTime $expire 有效时间 0为永久 199 | * @return bool 200 | */ 201 | public function set($name, $value, $expire = null): bool 202 | { 203 | $this->writeTimes++; 204 | 205 | if (is_null($expire)) { 206 | $expire = $this->options['expire']; 207 | } 208 | 209 | $expire = $this->getExpireTime($expire); 210 | $filename = $this->getCacheKey($name); 211 | 212 | $dir = dirname($filename); 213 | 214 | if (!is_dir($dir)) { 215 | try { 216 | mkdir($dir, 0755, true); 217 | } catch (\Exception $e) { 218 | // 创建失败 219 | } 220 | } 221 | 222 | $data = $this->serialize($value); 223 | 224 | if ($this->options['data_compress'] && function_exists('gzcompress')) { 225 | //数据压缩 226 | $data = gzcompress($data, 3); 227 | } 228 | 229 | $data = "\n" . $data; 230 | $result = file_put_contents($filename, $data); 231 | 232 | if ($result) { 233 | clearstatcache(); 234 | return true; 235 | } 236 | 237 | return false; 238 | } 239 | 240 | 241 | 242 | /** 243 | * 删除缓存 244 | * @access public 245 | * @param string $name 缓存变量名 246 | * @return bool 247 | */ 248 | public function delete($name): bool 249 | { 250 | $this->writeTimes++; 251 | 252 | return DirAndFile::unlink($this->getCacheKey($name)); 253 | } 254 | 255 | /** 256 | * 清除缓存 257 | * @access public 258 | * @return bool 259 | */ 260 | public function clear(): bool 261 | { 262 | $this->writeTimes++; 263 | 264 | $dirname = $this->options['path'] . $this->options['prefix']; 265 | 266 | $this->rmdir($dirname); 267 | 268 | return true; 269 | } 270 | 271 | 272 | /** 273 | * 删除文件夹 274 | * @param $dirname 275 | * @return bool 276 | */ 277 | private function rmdir($dirname) 278 | { 279 | if (!is_dir($dirname)) { 280 | return false; 281 | } 282 | 283 | $items = new FilesystemIterator($dirname); 284 | 285 | foreach ($items as $item) { 286 | if ($item->isDir() && !$item->isLink()) { 287 | $this->rmdir($item->getPathname()); 288 | } else { 289 | DirAndFile::unlink($item->getPathname()); 290 | } 291 | } 292 | 293 | @rmdir($dirname); 294 | 295 | return true; 296 | } 297 | 298 | } 299 | -------------------------------------------------------------------------------- /src/parses/ParseAnnotation.php: -------------------------------------------------------------------------------- 1 | parser = new AnnotationReader(); 22 | if (!empty($config['ignored_annitation'])){ 23 | foreach ($config['ignored_annitation'] as $item) { 24 | AnnotationReader::addGlobalIgnoredName($item); 25 | } 26 | } 27 | } 28 | /** 29 | * 解析非@注解的文本注释 30 | * @param $refMethod 31 | * @param $isAll bool 是否获取全部,true则将带@开头的注释也包含 32 | * @return array|false 33 | */ 34 | public static function parseTextAnnotation($refMethod,$isAll=false): array 35 | { 36 | $annotation = $refMethod->getDocComment(); 37 | if (empty($annotation)) { 38 | return []; 39 | } 40 | if (preg_match('#^/\*\*(.*)\*/#s', $annotation, $comment) === false) 41 | return []; 42 | $comment = trim($comment [1]); 43 | if (preg_match_all('#^\s*\*(.*)#m', $comment, $lines) === false) 44 | return []; 45 | $data = []; 46 | foreach ($lines[1] as $line) { 47 | $line = trim($line); 48 | if (!empty ($line) && ($isAll===true || ($isAll===false && strpos($line, '@') !== 0))) { 49 | $data[] = $line; 50 | } 51 | } 52 | return $data; 53 | } 54 | 55 | /** 56 | * 根据路径获取类名 57 | * @param $path 58 | * @return string 59 | */ 60 | protected function getClassName($path){ 61 | $NameArr = explode("\\", $path); 62 | $name = lcfirst($NameArr[count($NameArr) - 1]); 63 | return $name; 64 | } 65 | 66 | /** 67 | * 获取并处理注解参数 68 | * @param $attrList 69 | * @return array 70 | */ 71 | protected function getParameters($attrList){ 72 | $attrs = []; 73 | foreach ($attrList as $item) { 74 | $value = ""; 75 | if ($item instanceof ReflectionAttribute) { 76 | $attributeName = $item->getName(); 77 | if (strpos($attributeName, 'apidoc') === false){ 78 | continue; 79 | } 80 | $name = $this->getClassName($attributeName); 81 | $params = $item->getArguments(); 82 | if (!empty($params)){ 83 | if (is_array($params) && !empty($params[0]) && is_string($params[0]) && count($params)===1){ 84 | $value = $params[0]; 85 | }else{ 86 | if (isset($params[0])){ 87 | $paramObj = []; 88 | foreach ($params as $k=>$value) { 89 | $key = $k===0?'name':$k; 90 | $paramObj[$key]=$value; 91 | } 92 | }else{ 93 | $paramObj = $params; 94 | } 95 | $value = $paramObj; 96 | } 97 | } 98 | }else{ 99 | $name = $this->getClassName(get_class($item)); 100 | $valueObj = Helper::objectToArray($item); 101 | if (array_key_exists('name',$valueObj) && count($valueObj)===1){ 102 | $value = $valueObj['name']===null?true: $valueObj['name']; 103 | }else{ 104 | $value = $valueObj; 105 | } 106 | } 107 | if (!empty($attrs[$name]) && is_array($attrs[$name]) && Helper::arrayKeyFirst($attrs[$name])===0){ 108 | $attrs[$name][]=$value; 109 | }else if(!empty($attrs[$name])){ 110 | $attrs[$name] = [$attrs[$name],$value]; 111 | }else{ 112 | $attrs[$name]=$value; 113 | } 114 | } 115 | return $attrs; 116 | } 117 | 118 | /** 119 | * 获取类的注解参数 120 | * @param ReflectionMethod $refMethod 121 | * @return array 122 | */ 123 | public function getClassAnnotation($refClass){ 124 | if (method_exists($refClass,'getAttributes')){ 125 | $attributes = $refClass->getAttributes(); 126 | }else{ 127 | $attributes = []; 128 | } 129 | $readerAttributes = $this->parser->getClassAnnotations($refClass); 130 | return $this->getParameters(array_merge($attributes,$readerAttributes)); 131 | } 132 | 133 | /** 134 | * 获取方法的注解参数 135 | * @param ReflectionMethod $refMethod 136 | * @return array 137 | */ 138 | public function getMethodAnnotation(ReflectionMethod $refMethod){ 139 | if (method_exists($refMethod,'getAttributes')){ 140 | $attributes = $refMethod->getAttributes(); 141 | }else{ 142 | $attributes = []; 143 | } 144 | $readerAttributes = $this->parser->getMethodAnnotations($refMethod); 145 | return $this->getParameters(array_merge($attributes,$readerAttributes)); 146 | } 147 | 148 | /** 149 | * 获取属性的注解参数 150 | * @param $property 151 | * @return array 152 | */ 153 | public function getPropertyAnnotation($property){ 154 | if (method_exists($property,'getAttributes')){ 155 | $attributes = $property->getAttributes(); 156 | }else{ 157 | $attributes = []; 158 | } 159 | $readerAttributes = $this->parser->getPropertyAnnotations($property); 160 | return $this->getParameters(array_merge($attributes,$readerAttributes)); 161 | } 162 | 163 | /** 164 | * 解析类的属性文本注释的var 165 | * @param $propertyTextAnnotations 166 | * @return array 167 | */ 168 | protected static function parsesPropertyTextAnnotation($propertyTextAnnotations){ 169 | $varLine = ""; 170 | foreach ($propertyTextAnnotations as $item) { 171 | if (strpos($item, '@var') !== false){ 172 | $varLine = $item; 173 | break; 174 | } 175 | } 176 | $type = ""; 177 | $desc = ""; 178 | if ($varLine){ 179 | $varLineArr = preg_split('/\\s+/', $varLine); 180 | $type = !empty($varLineArr[1])?$varLineArr[1]:""; 181 | $desc = !empty($varLineArr[2])?$varLineArr[2]:""; 182 | } 183 | if (empty($desc) && strpos($propertyTextAnnotations[0], '@var') === false){ 184 | $desc = $propertyTextAnnotations[0]; 185 | } 186 | return [ 187 | 'type'=>$type, 188 | 'desc'=>$desc, 189 | ]; 190 | } 191 | 192 | /** 193 | * 获取类的属性参数 194 | * @param $classReflect 195 | * @return array 196 | */ 197 | public function getClassPropertiesy($classReflect){ 198 | $publicProperties = $classReflect->getProperties(\ReflectionProperty::IS_PUBLIC); 199 | $arr=[]; 200 | foreach ($publicProperties as $property) { 201 | $propertyAnnotations = $this->getPropertyAnnotation($property); 202 | $item = []; 203 | if (!empty($propertyAnnotations['property'])){ 204 | // 有apidoc注解 205 | $arr[] = $propertyAnnotations['property']; 206 | continue; 207 | } 208 | $propertyTextAnnotations = self::parseTextAnnotation($property,true); 209 | if (empty($propertyTextAnnotations)){ 210 | // 无注释 211 | continue; 212 | } 213 | $textAnnotationsParams=static::parsesPropertyTextAnnotation($propertyTextAnnotations); 214 | $textAnnotationsParams['name'] =$property->getName(); 215 | $arr[]=$textAnnotationsParams; 216 | } 217 | return $arr; 218 | } 219 | 220 | 221 | 222 | } -------------------------------------------------------------------------------- /src/utils/DirAndFile.php: -------------------------------------------------------------------------------- 1 | $value, 20 | 'path'=>$sub_path, 21 | ]; 22 | $children = static::getDirTree($sub_path); 23 | if (count($children)){ 24 | $item['children'] = $children; 25 | } 26 | $arr[] = $item; 27 | } 28 | } 29 | } 30 | return $arr; 31 | } 32 | 33 | public static function getClassList($dir){ 34 | if ($handle = opendir($dir)) { 35 | $file_list=[]; 36 | while (false !== ($file = readdir($handle))) { 37 | if($file=='..' || $file=='.') continue; 38 | $filePath = static::formatPath($dir.'/'.$file,"/"); 39 | if(is_file($filePath)) { 40 | if ('php' !== pathinfo($filePath, \PATHINFO_EXTENSION)) { 41 | continue; 42 | } 43 | $classes = self::findClasses($filePath); 44 | if (!empty($classes) && count($classes)){ 45 | $file_list[] = [ 46 | 'name'=>$classes[0], 47 | 'path'=>$filePath 48 | ]; 49 | }else{ 50 | $file_list=[]; 51 | } 52 | continue; 53 | } 54 | $file_list[$file] = static::getClassList($filePath); 55 | foreach($file_list[$file] as $infile) { 56 | $file_list[] = $infile; 57 | } 58 | unset($file_list[$file]); 59 | } 60 | closedir($handle); 61 | return $file_list; 62 | } 63 | return []; 64 | } 65 | public static function getFileList($path){ 66 | if(is_dir($path)) { 67 | $dirList = scandir($path); 68 | $list = []; 69 | foreach ($dirList as $dir) { 70 | if ($dir == '.' || $dir == '..') { 71 | continue; 72 | } 73 | $sub_path = DirAndFile::formatPath($path . '/' . $dir, "/"); 74 | if (is_file($sub_path)){ 75 | $list[]=[ 76 | 'name'=>$dir, 77 | 'path'=>$sub_path 78 | ];; 79 | } 80 | } 81 | return $list; 82 | } 83 | return []; 84 | } 85 | 86 | public static function formatPath($path,$type="/"){ 87 | if ($type==="/"){ 88 | $path = str_replace("\\","/",$path); 89 | }else{ 90 | $path = str_replace("/","\\",$path); 91 | $path = str_replace("\\\\","\\",$path); 92 | $endStr = substr($path, -1); 93 | if ($endStr=='\\'){ 94 | $path = substr($path,0,strlen($path)-1); 95 | } 96 | } 97 | return $path; 98 | } 99 | 100 | private static function findClasses($path) 101 | { 102 | $contents = file_get_contents($path); 103 | $tokens = token_get_all($contents); 104 | 105 | $nsTokens = [\T_STRING => true, \T_NS_SEPARATOR => true]; 106 | if (\defined('T_NAME_QUALIFIED')) { 107 | $nsTokens[T_NAME_QUALIFIED] = true; 108 | } 109 | 110 | $classes = []; 111 | 112 | $namespace = ''; 113 | for ($i = 0; isset($tokens[$i]); ++$i) { 114 | $token = $tokens[$i]; 115 | 116 | if (!isset($token[1])) { 117 | continue; 118 | } 119 | 120 | $class = ''; 121 | 122 | switch ($token[0]) { 123 | case \T_NAMESPACE: 124 | $namespace = ''; 125 | // If there is a namespace, extract it 126 | while (isset($tokens[++$i][1])) { 127 | if (isset($nsTokens[$tokens[$i][0]])) { 128 | $namespace .= $tokens[$i][1]; 129 | } 130 | } 131 | $namespace .= '\\'; 132 | break; 133 | case \T_CLASS: 134 | case \T_INTERFACE: 135 | case \T_TRAIT: 136 | // Skip usage of ::class constant 137 | $isClassConstant = false; 138 | for ($j = $i - 1; $j > 0; --$j) { 139 | if (!isset($tokens[$j][1])) { 140 | break; 141 | } 142 | 143 | if (\T_DOUBLE_COLON === $tokens[$j][0]) { 144 | $isClassConstant = true; 145 | break; 146 | } elseif (!\in_array($tokens[$j][0], [\T_WHITESPACE, \T_DOC_COMMENT, \T_COMMENT])) { 147 | break; 148 | } 149 | } 150 | 151 | if ($isClassConstant) { 152 | break; 153 | } 154 | 155 | // Find the classname 156 | while (isset($tokens[++$i][1])) { 157 | $t = $tokens[$i]; 158 | if (\T_STRING === $t[0]) { 159 | $class .= $t[1]; 160 | } elseif ('' !== $class && \T_WHITESPACE === $t[0]) { 161 | break; 162 | } 163 | } 164 | 165 | $classes[] = ltrim($namespace.$class, '\\'); 166 | break; 167 | default: 168 | break; 169 | } 170 | } 171 | 172 | return $classes; 173 | } 174 | 175 | 176 | /** 177 | * 读取文件内容 178 | * @param $fileName 179 | * @return false|string 180 | */ 181 | public static function getFileContent(string $fileName): string 182 | { 183 | $content = ""; 184 | if (file_exists($fileName)) { 185 | $handle = fopen($fileName, "r"); 186 | $content = fread($handle, filesize($fileName)); 187 | fclose($handle); 188 | } 189 | return $content; 190 | } 191 | 192 | /** 193 | * 保存文件 194 | * @param $path 195 | * @param $str_tmp 196 | * @return bool 197 | */ 198 | public static function createFile(string $path, string $str_tmp): bool 199 | { 200 | $pathArr = explode("/", $path); 201 | unset($pathArr[count($pathArr) - 1]); 202 | $dir = implode("/", $pathArr); 203 | if (!file_exists($dir)) { 204 | mkdir($dir, 0775, true); 205 | } 206 | $fp = fopen($path, "w") or die("Unable to open file!"); 207 | fwrite($fp, $str_tmp); //存入内容 208 | fclose($fp); 209 | return true; 210 | } 211 | 212 | /** 213 | * 判断文件是否存在后,删除 214 | * @access private 215 | * @param string $path 216 | * @return bool 217 | */ 218 | public static function unlink(string $path): bool 219 | { 220 | try { 221 | return is_file($path) && unlink($path); 222 | } catch (\Exception $e) { 223 | return false; 224 | } 225 | } 226 | 227 | public static function checkFileExist(string $path) 228 | { 229 | try { 230 | return $path; 231 | } catch (\Exception $e) { 232 | return $e; 233 | } 234 | } 235 | 236 | public static function deleteDir($path) { 237 | if (!is_dir($path)) { 238 | return false; 239 | } 240 | $open = opendir($path); 241 | if (!$open) { 242 | return false; 243 | } 244 | while (($v = readdir($open)) !== false) { 245 | if ('.' == $v || '..' == $v) { 246 | continue; 247 | } 248 | $item = $path . '/' . $v; 249 | if (is_file($item)) { 250 | unlink($item); 251 | continue; 252 | } 253 | static::deleteDir($item); 254 | } 255 | closedir($open); 256 | return rmdir($path); 257 | } 258 | } -------------------------------------------------------------------------------- /src/parses/ParseApiMenus.php: -------------------------------------------------------------------------------- 1 | config = $config; 34 | } 35 | 36 | /** 37 | * 生成api接口数据 38 | * @param string $appKey 39 | * @param bool $isParseDetail 是否解析接口明细 40 | * @return array 41 | */ 42 | public function renderApiMenus(string $appKey,bool $isParseDetail=false): array 43 | { 44 | $currentAppConfig = Helper::getCurrentAppConfig($appKey); 45 | $currentApp = $currentAppConfig['appConfig']; 46 | $this->currentApp = $currentApp; 47 | $this->appKey = $appKey; 48 | 49 | $controllers = []; 50 | if (!empty($currentApp['controllers']) && count($currentApp['controllers']) > 0) { 51 | // 配置的控制器列表 52 | $controllers = $this->getConfigControllers($currentApp['path'],$currentApp['controllers']); 53 | }else if(!empty($currentApp['path']) && is_array($currentApp['path']) && count($currentApp['path'])){ 54 | // 读取paths的 55 | foreach ($currentApp['path'] as $path) { 56 | $controllersList = $this->getDirControllers($path); 57 | $controllers = array_merge($controllers,$controllersList); 58 | } 59 | } else if(!empty($currentApp['path']) && is_string($currentApp['path'])){ 60 | // 默认读取path下所有的 61 | $controllers = $this->getDirControllers($currentApp['path']); 62 | } 63 | $apiData = []; 64 | if (!empty($controllers) && count($controllers) > 0) { 65 | foreach ($controllers as $class) { 66 | $classData = $this->parseController($class,$isParseDetail); 67 | if ($classData !== false) { 68 | $apiData[] = $classData; 69 | } 70 | } 71 | } 72 | // 排序 73 | $apiList = Helper::arraySortByKey($apiData); 74 | 75 | // 接口分组 76 | if (!empty($currentApp['groups'])){ 77 | $apiList = ParseApiMenus::mergeApiGroup($apiList,$currentApp['groups']); 78 | } 79 | 80 | $json = array( 81 | "data" => $apiList, 82 | "tags" => $this->tags, 83 | "groups" => $this->groups, 84 | ); 85 | return $json; 86 | } 87 | 88 | /** 89 | * 获取生成文档的控制器列表 90 | * @param string $path 91 | * @return array 92 | */ 93 | public function getConfigControllers(string $path,$appControllers): array 94 | { 95 | $controllers = []; 96 | if (!empty($appControllers) && count($appControllers) > 0) { 97 | foreach ($appControllers as $item) { 98 | $classPath = $path."\\".$item; 99 | if ( class_exists($classPath)) { 100 | $controllers[] = $classPath; 101 | } 102 | } 103 | } 104 | return $controllers; 105 | } 106 | 107 | /** 108 | * 获取目录下的控制器列表 109 | * @param string $path 110 | * @return array 111 | */ 112 | public function getDirControllers(string $path): array 113 | { 114 | 115 | if ($path) { 116 | if (strpos(APIDOC_ROOT_PATH, '/') !== false) { 117 | $pathStr = str_replace("\\", "/", $path); 118 | } else { 119 | $pathStr = $path; 120 | } 121 | $dir = APIDOC_ROOT_PATH . $pathStr; 122 | } else { 123 | $dir = APIDOC_ROOT_PATH . $this->controller_layer; 124 | } 125 | $controllers = []; 126 | if (is_dir($dir)) { 127 | $controllers = $this->scanDir($dir, $path); 128 | } 129 | return $controllers; 130 | } 131 | 132 | 133 | 134 | protected function scanDir($dir) { 135 | 136 | $classList= DirAndFile::getClassList($dir); 137 | $list=[]; 138 | 139 | $configFilterController = !empty($this->config['filter_controllers']) ? $this->config['filter_controllers'] : []; 140 | $currentAppFilterController = !empty($this->currentApp['filter_controllers']) ? $this->currentApp['filter_controllers'] : []; 141 | $filterControllers = array_merge($configFilterController,$currentAppFilterController); 142 | 143 | $configFilterDir = !empty($this->config['filter_dirs']) ? $this->config['filter_dirs'] : []; 144 | $currentAppFilterDir = !empty($this->currentApp['filter_dirs']) ? $this->currentApp['filter_dirs'] : []; 145 | $filterDirList = array_merge($configFilterDir,$currentAppFilterDir); 146 | $filterDirs=[]; 147 | foreach ($filterDirList as $dirItem) { 148 | $dirItemPath = DirAndFile::formatPath($dirItem,"/"); 149 | $filterDirs[]=$dirItemPath; 150 | } 151 | 152 | foreach ($classList as $item) { 153 | $classNamespace = $item['name']; 154 | 155 | $isFilterDir = false; 156 | foreach ($filterDirs as $dirItem) { 157 | if (strpos($item['path'], $dirItem) !== false){ 158 | $isFilterDir=true; 159 | } 160 | } 161 | if ($isFilterDir){ 162 | continue; 163 | }else if ( 164 | !in_array($classNamespace, $filterControllers) && 165 | $this->config['definitions'] != $classNamespace 166 | 167 | ) { 168 | $list[] = $classNamespace; 169 | } 170 | } 171 | 172 | return $list; 173 | } 174 | 175 | public function parseController($class,$isParseDetail=false) 176 | { 177 | 178 | 179 | $refClass = new ReflectionClass($class); 180 | $classTextAnnotations = ParseAnnotation::parseTextAnnotation($refClass); 181 | $data = (new ParseAnnotation($this->config))->getClassAnnotation($refClass); 182 | if (in_array("NotParse", $classTextAnnotations) || isset($data['notParse'])) { 183 | return false; 184 | } 185 | $controllersName = $refClass->getShortName(); 186 | $data['controller'] = $controllersName; 187 | $data['path'] = $class; 188 | $data['appKey'] = $this->appKey; 189 | if (!empty($data['group']) && !in_array($data['group'], $this->groups)) { 190 | $this->groups[] = $data['group']; 191 | } 192 | 193 | if (empty($data['title'])) { 194 | if (!empty($classTextAnnotations) && count($classTextAnnotations) > 0) { 195 | $data['title'] = $classTextAnnotations[0]; 196 | } else { 197 | $data['title'] = $controllersName; 198 | } 199 | } 200 | $data['title'] = Lang::getLang($data['title']); 201 | $methodList = []; 202 | $data['menuKey'] = Helper::createRandKey($data['controller']); 203 | $isNotDebug = in_array("NotDebug", $classTextAnnotations) || isset($data['notDebug']); 204 | $parseApiDetail = new ParseApiDetail($this->config); 205 | foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $refMethod) { 206 | if ($isParseDetail){ 207 | $methodItem = $parseApiDetail->parseApiMethod($refClass,$refMethod,$this->currentApp); 208 | }else{ 209 | $methodItem = $this->parseApiMethod($refClass,$refMethod); 210 | } 211 | if ($methodItem===false){ 212 | continue; 213 | } 214 | if ($isNotDebug) { 215 | $methodItem['notDebug'] = true; 216 | } 217 | $methodList[] = $methodItem; 218 | } 219 | $data['children'] = $methodList; 220 | if (count($methodList)===0){ 221 | return false; 222 | } 223 | return $data; 224 | } 225 | 226 | 227 | protected function parseApiMethod($refClass,$refMethod){ 228 | $config = $this->config; 229 | if (empty($refMethod->name)) { 230 | return false; 231 | } 232 | if(!empty($config['ignored_methods']) && in_array($refMethod->name, $config['ignored_methods'])){ 233 | return false; 234 | } 235 | 236 | try { 237 | $textAnnotations = ParseAnnotation::parseTextAnnotation($refMethod); 238 | 239 | $methodAnnotation = (new ParseAnnotation($this->config))->getMethodAnnotation($refMethod); 240 | // 标注不解析的方法 241 | if (in_array("NotParse", $textAnnotations) || isset($methodAnnotation['notParse']) || empty($methodAnnotation)) { 242 | return false; 243 | } 244 | $methodInfo = ParseApiDetail::handleApiBaseInfo($methodAnnotation,$refClass->name,$refMethod->name,$textAnnotations,$config); 245 | $methodInfo['appKey'] = !empty($this->currentApp['appKey'])?$this->currentApp['appKey']:""; 246 | return Helper::getArrayValuesByKeys($methodInfo,['title','method','url','author','tag','name','menuKey','appKey']); 247 | }catch (AnnotationException $e) { 248 | throw new ErrorException($e->getMessage()); 249 | } 250 | 251 | } 252 | 253 | 254 | 255 | /** 256 | * 对象分组到tree 257 | * @param $tree 258 | * @param $objectData 259 | * @param string $childrenField 260 | * @return array 261 | */ 262 | public static function objtctGroupByTree($tree,$objectData,$childrenField='children'){ 263 | $data = []; 264 | foreach ($tree as $node){ 265 | if (!empty($node[$childrenField])){ 266 | $node[$childrenField] = static::objtctGroupByTree($node[$childrenField],$objectData); 267 | }else if (!empty($objectData[$node['name']])){ 268 | $node[$childrenField] = $objectData[$node['name']]; 269 | } 270 | $node['menuKey'] = Helper::createRandKey( $node['name']); 271 | $data[] = $node; 272 | } 273 | return $data; 274 | } 275 | 276 | protected static function getAppGroupNames($groups){ 277 | $groupNames = []; 278 | foreach ($groups as $item) { 279 | if (!empty($item['name'])){ 280 | $groupNames[]=$item['name']; 281 | } 282 | if (!empty($item['children']) && count($item['children'])){ 283 | $childrenNames = self::getAppGroupNames($item['children']); 284 | foreach ($childrenNames as $childrenName) { 285 | $groupNames[]=$childrenName; 286 | } 287 | } 288 | } 289 | return $groupNames; 290 | } 291 | 292 | /** 293 | * 合并接口到应用分组 294 | * @param $apiData 295 | * @param $groups 296 | * @return array 297 | */ 298 | public static function mergeApiGroup($apiData,$groups){ 299 | if (empty($groups) || count($apiData)<1){ 300 | return $apiData; 301 | } 302 | $groupNames = static::getAppGroupNames($groups); 303 | $apiObject = []; 304 | foreach ($apiData as $controller){ 305 | if (!empty($controller['group']) && in_array($controller['group'],$groupNames)){ 306 | if (!empty($apiObject[$controller['group']])){ 307 | $apiObject[$controller['group']][] = $controller; 308 | }else{ 309 | $apiObject[$controller['group']] = [$controller]; 310 | } 311 | }else{ 312 | if (!empty($apiObject['notGroup'])){ 313 | $apiObject['notGroup'][] = $controller; 314 | }else{ 315 | $apiObject['notGroup'] = [$controller]; 316 | } 317 | } 318 | } 319 | if (!empty($apiObject['notGroup'])){ 320 | array_unshift($groups,['title'=>'未分组','name'=>'notGroup']); 321 | } 322 | $res = static::objtctGroupByTree($groups,$apiObject); 323 | return $res; 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/generator/ParseTemplate.php: -------------------------------------------------------------------------------- 1 | replaceForeach($tplContent,$params); 18 | $tplContent = $this->replaceParams($tplContent,$params); 19 | $tplContent = $this->replaceIf($tplContent,$params); 20 | $tplContent = preg_replace("/\s+\r\n/is", "\r\n", $tplContent); 21 | return $tplContent; 22 | } 23 | 24 | /** 25 | * 替换变量 26 | * @param $tplContent 27 | * @param $params 28 | * @return array|string|string[]|null 29 | */ 30 | protected function replaceParams($tplContent,$params){ 31 | $key = '{$%%}'; 32 | $pattern = '#' . str_replace('%%', '(.+?)' , preg_quote($key, '#')) . '#'; 33 | $tplContent = preg_replace_callback($pattern, function ($matches)use ($params){ 34 | $k = $matches[1]; 35 | if (strpos($k, '(') !== false){ 36 | $tagArr = explode("(", $k); 37 | $fun = $tagArr[0]; 38 | $k = str_replace(")", "",$tagArr[1] ); 39 | $v = $this->getObjectValueByKeys($params,$k); 40 | if (empty($v)){ 41 | return ""; 42 | } 43 | if ($fun === "lower"){ 44 | return Helper::lower($v); 45 | }else if ($fun === "snake"){ 46 | return Helper::snake($v); 47 | }else if ($fun === "lcfirst"){ 48 | return lcfirst($v); 49 | }else if ($fun === "ucfirst"){ 50 | return ucfirst($v); 51 | }else if ($fun === "count"){ 52 | return count($v); 53 | } 54 | } 55 | $value = $this->getObjectValueByKeys($params,$k); 56 | if (is_bool($value)){ 57 | return $value==true?'true':'false'; 58 | }else if (is_array($value)){ 59 | return $k; 60 | } 61 | return $value; 62 | }, $tplContent); 63 | return $tplContent; 64 | } 65 | 66 | /** 67 | * 替换if内容 68 | * @param $tplContent 69 | * @param $params 70 | * @return array|mixed|string|string[] 71 | * @throws \Exception 72 | */ 73 | protected function replaceIf($tplContent,$params){ 74 | $res = []; 75 | $label = "if"; 76 | $labelList = $this->parseLabel($tplContent,$label); 77 | if (!empty($labelList) && count($labelList)>0){ 78 | foreach ($labelList as $item) { 79 | $itemStr =$item; 80 | $ifChildren= $this->parseLabel($itemStr,$label,"children"); 81 | 82 | if (!empty($ifChildren) && count($ifChildren)>0){ 83 | foreach ($ifChildren as $ifChild){ 84 | $itemChildrenContent= $this->getIfContent($ifChild); 85 | $itemStr = str_replace($ifChild, $itemChildrenContent,$itemStr ); 86 | } 87 | } 88 | $itemContent= $this->getIfContent($itemStr); 89 | $tplContent = str_replace($item, $itemContent,$tplContent ); 90 | } 91 | } 92 | 93 | return $tplContent; 94 | } 95 | protected function parseForeach($str,$params){ 96 | if (preg_match('#{foreach (.+?) as (.+?)=>(.+?)}#s', $str, $matches)){ 97 | $complete = $matches[0]; 98 | $condition = $matches[1]; 99 | $keyField = str_replace("$", "",$matches[2] ); 100 | $itemField = str_replace("$", "",$matches[3] ); 101 | $conditionKey = str_replace("$", "",$condition ); 102 | $forListData = $this->getObjectValueByKeys($params,$conditionKey); 103 | $contentStr = str_replace($complete, "",$str); 104 | $contentStr = substr($contentStr,0,-10); 105 | return [ 106 | 'list'=>$forListData, 107 | 'keyField'=>$keyField, 108 | 'itemField'=>$itemField, 109 | 'content'=>$contentStr 110 | ]; 111 | } 112 | return []; 113 | } 114 | 115 | /** 116 | * 获取所有foreach标签 117 | * @param $str 118 | * @return array 119 | * @throws \Exception 120 | */ 121 | protected function getAllForeachLabel($str){ 122 | $tree = []; 123 | $label = "foreach"; 124 | $labelList = $this->parseLabel($str,$label); 125 | if (!empty($labelList) && count($labelList)>0){ 126 | foreach ($labelList as $itemLabel) { 127 | $labelChildrenList = $this->parseLabel($itemLabel,$label,"children"); 128 | if (!empty($labelChildrenList) && count($labelChildrenList)>0){ 129 | $childrenList = []; 130 | foreach ($labelChildrenList as $item) { 131 | $childrenList[]=[ 132 | 'str'=>$item, 133 | 'children' => [] 134 | ]; 135 | } 136 | $tree[]=[ 137 | 'str'=>$itemLabel, 138 | 'children' => $childrenList 139 | ]; 140 | }else{ 141 | $tree[]=[ 142 | 'str'=>$itemLabel, 143 | 'children' => [] 144 | ]; 145 | } 146 | } 147 | } 148 | return $tree; 149 | } 150 | // 解析foreach 151 | protected function replaceForeach($html,$params,$level=""){ 152 | $allLabelData= $this->getAllForeachLabel($html); 153 | $res = []; 154 | if (count($allLabelData)>0){ 155 | // 遍历每个foreach标签 156 | foreach ($allLabelData as $labelItem) { 157 | $itemStr = $labelItem['str']; 158 | $forOption = $this->parseForeach($labelItem['str'],$params); 159 | $itemContent=""; 160 | if (!empty($forOption['list']) && count($forOption['list'])>0){ 161 | // 处理每行数据 162 | foreach ($forOption['list'] as $rowKey=>$row) { 163 | $rowData = [$forOption['itemField']=>$row,$forOption['keyField']=>$rowKey]; 164 | $rowParams = array_merge($params,$rowData); 165 | // 存在子标签,处理子标签 166 | if (!empty($labelItem['children']) && count($labelItem['children'])>0){ 167 | $itemStrContent = ""; 168 | foreach ($labelItem['children'] as $childLabel){ 169 | $childContents = ""; 170 | $childStr = $childLabel['str']; 171 | $childDataList = $this->parseForeach($childLabel['str'],$rowParams); 172 | // 处理子标签数据 173 | if (!empty($childDataList['list']) && count($childDataList['list'])>0){ 174 | foreach ($childDataList['list'] as $childDataKey=>$childDataItem) { 175 | // 子标签每行数据 176 | $childDataItemData = [$childDataList['itemField']=>$childDataItem,$childDataList['keyField']=>$childDataKey,]; 177 | $contentsStr= $this->getForContent($childDataList['content'],array_merge($rowParams,$childDataItemData)); 178 | $contentsStr =ltrim($contentsStr,"\r\n"); 179 | if (!empty(Helper::trimEmpty($contentsStr))){ 180 | $childContents.= $contentsStr; 181 | } 182 | } 183 | } 184 | $itemStrContent.= str_replace($childLabel['str'], $childContents,$forOption['content']); 185 | } 186 | $rowContent=$this->replaceParams($itemStrContent,$rowParams); 187 | $itemContentStr=$this->replaceIf($rowContent,$rowParams); 188 | if (!empty(Helper::trimEmpty($itemContentStr))){ 189 | $itemContent.= $itemContentStr; 190 | } 191 | }else{ 192 | $rowContent=$this->getForContent($forOption['content'],$rowParams); 193 | if (empty(Helper::trimEmpty($rowContent))){ 194 | $rowContent= ""; 195 | } 196 | $itemContent.= $rowContent; 197 | } 198 | $itemContent =trim($itemContent,"\r\n"); 199 | } 200 | } 201 | $html = str_replace($labelItem['str'], $itemContent,$html ); 202 | } 203 | } 204 | return $html; 205 | 206 | } 207 | 208 | /** 209 | * 获取foreach内容 210 | * @param $str 211 | * @param $params 212 | * @return array|mixed|string|string[] 213 | * @throws \Exception 214 | */ 215 | protected function getForContent($str,$params){ 216 | $content = $str; 217 | if (!empty($params)){ 218 | $content = $this->replaceParams($content,$params); 219 | $content = $this->replaceIf($content,$params); 220 | } 221 | return $content; 222 | } 223 | 224 | 225 | /** 226 | * 获取if条件的内容 227 | * @param $str 228 | * @return mixed|string 229 | */ 230 | protected function getIfContent($str){ 231 | if (preg_match('#{if (.+?)}(.*?){/if}#s', $str, $matches)){ 232 | if (eval("return $matches[1];")){ 233 | // 条件成立 234 | return $matches[2]; 235 | } 236 | } 237 | return ""; 238 | } 239 | 240 | 241 | /** 242 | * 解析指定标签 243 | * @param $str 244 | * @param $label 245 | * @param string $level 246 | * @return array 247 | * @throws \Exception 248 | */ 249 | protected function parseLabel($str,$label,$level=""){ 250 | // 后面的 flag 表示记录偏移量 251 | preg_match_all('!({/?'.$label.' ?}?)!', $str, $matches, PREG_OFFSET_CAPTURE); 252 | // 用数组来模拟栈 253 | $stack = []; 254 | $top = null; 255 | $result = []; 256 | foreach ($matches[0] as $k=>[$match, $offset]) { 257 | // 当取标签内容时,排除第一个和最后一个标签 258 | if ($level === 'children' && ($k==0 || $k>=count($matches[0])-1)){ 259 | continue; 260 | } 261 | // 判断匹配到的如果是 开始标签 262 | if ($match === '{'.$label.' ') { 263 | $stack[] = $offset; 264 | // 记录开始的位置 265 | if ($top === null) { 266 | $top = $offset; 267 | } 268 | // 如果不是 269 | } else { 270 | // 从栈底部拿一个出来 271 | $pop = array_pop($stack); 272 | // 如果取出来的是 null 就说明存在多余的 标签 273 | if ($pop === null) { 274 | throw new \Exception('语法错误,存在多余的 {/'.$label.'} 标签'); 275 | } 276 | // 如果取完后栈空了 277 | if (empty($stack)) { 278 | // offset 是匹配到的开始标签(前面)位置,加上内容的长度 279 | $newOffset = $offset + strlen($match)-$top; 280 | // 从顶部到当前的偏移就是这个标签里的内容 281 | $result[] = substr($str, $top, $newOffset); 282 | // 重置 top 开始下一轮 283 | $top = null; 284 | } 285 | } 286 | } 287 | // 如果运行完了,栈里面还有东西,那就说明缺少闭合标签。 288 | if (!empty($stack)) { 289 | throw new \Exception('语法错误,存在未闭合的 {/'.$label.'} 标签'); 290 | } 291 | return $result; 292 | } 293 | 294 | /** 295 | * 根据keys获取对象中的值 296 | * @param $array 297 | * @param $keyStr 298 | * @param string $delimiter 299 | * @return mixed|null 300 | */ 301 | public function getObjectValueByKeys($array, $keyStr, $delimiter = '.') 302 | { 303 | $keys = explode($delimiter, $keyStr); 304 | if (preg_match_all('#\[(.+?)]#s', $keyStr, $matches)){ 305 | $value = $array; 306 | if (!empty($matches[1])){ 307 | $matchesIndex=0; 308 | foreach ($keys as $keyItem) { 309 | if (strpos($keyItem, '[') !== false) { 310 | $tagArr = explode("[", $keyItem); 311 | if (!empty($value[$tagArr[0]]) && !empty($value[$tagArr[0]][$matches[1][$matchesIndex]])){ 312 | $value =$value[$tagArr[0]][$matches[1][$matchesIndex]]; 313 | }else{ 314 | $value =null; 315 | break; 316 | } 317 | $matchesIndex=$matchesIndex+1; 318 | }else{ 319 | $value =$value[$keyItem]; 320 | } 321 | } 322 | } 323 | return $value; 324 | }else if (sizeof($keys) > 1) { 325 | $value = $array; 326 | foreach ($keys as $key){ 327 | if (!empty($value[$key])){ 328 | $value = $value[$key]; 329 | }else{ 330 | $value =null; 331 | break; 332 | } 333 | } 334 | return $value; 335 | } else { 336 | return $array[$keyStr] ?? null; 337 | } 338 | } 339 | 340 | 341 | } -------------------------------------------------------------------------------- /src/generator/Index.php: -------------------------------------------------------------------------------- 1 | '', 18 | // 数据库编码,默认为utf8 19 | 'charset' => 'utf8', 20 | // 数据库引擎,默认为 InnoDB 21 | 'engine' => 'InnoDB', 22 | ]; 23 | 24 | protected $systemDefaultValues = [ 25 | 'CURRENT_TIMESTAMP' 26 | ]; 27 | 28 | public function __construct($config) 29 | { 30 | $this->config = $config; 31 | if (!empty($config['database'])){ 32 | if (!empty($config['database']['prefix'])){ 33 | $this->databaseConfig['prefix'] = $config['database']['prefix']; 34 | } 35 | if (!empty($config['database']['charset'])){ 36 | $this->databaseConfig['charset'] = $config['database']['charset']; 37 | } 38 | if (!empty($config['database']['engine'])){ 39 | $this->databaseConfig['engine'] = $config['database']['engine']; 40 | } 41 | } 42 | } 43 | 44 | public function create($params){ 45 | $appKey = $params['form']['appKey']; 46 | $currentAppConfig = Helper::getCurrentAppConfig($appKey); 47 | $currentApps = $currentAppConfig['apps']; 48 | $currentApp = $currentAppConfig['appConfig']; 49 | $generatorItem = $this->config['generator'][$params['index']]; 50 | 51 | $checkParams = $this->checkFilesAndHandleParams($generatorItem,$params,$currentApps); 52 | $tplParams = $checkParams['tplParams']; 53 | // 注册中间件并执行before 54 | if (!empty($generatorItem['middleware']) && count($generatorItem['middleware'])){ 55 | foreach ($generatorItem['middleware'] as $middleware) { 56 | $instance = new $middleware; 57 | $this->middlewares[] = $instance; 58 | if (method_exists($instance, 'before')) { 59 | $middlewareRes = $instance->before($tplParams); 60 | if (!empty($middlewareRes)){ 61 | $tplParams = $middlewareRes; 62 | } 63 | } 64 | } 65 | } 66 | 67 | $this->createModels($checkParams['createModels'],$tplParams); 68 | $this->createFiles($checkParams['createFiles'],$tplParams); 69 | //执行after 70 | if (count($this->middlewares)){ 71 | foreach ($this->middlewares as $middleware) { 72 | if (method_exists($instance, 'after')) { 73 | $instance->after($tplParams); 74 | } 75 | } 76 | } 77 | return $tplParams; 78 | } 79 | 80 | /** 81 | * 验证文件及处理模板数据 82 | * @param $generatorItem 83 | * @param $params 84 | * @param $currentApps 85 | * @return array 86 | */ 87 | protected function checkFilesAndHandleParams($generatorItem,$params,$currentApps){ 88 | // 组成模板参数 89 | $tplParams=[ 90 | 'form'=>$params['form'], 91 | 'tables'=>$params['tables'], 92 | 'app'=>$currentApps 93 | ]; 94 | $createFiles = []; 95 | if (!empty($params['files']) && count($params['files'])>0) { 96 | $files = $params['files']; 97 | foreach ($files as $file) { 98 | $fileConfig = Helper::getArrayFind($generatorItem['files'], function ($item) use ($file) { 99 | if ($file['name'] === $item['name']) { 100 | return true; 101 | } 102 | return false; 103 | }); 104 | 105 | $filePath = $file['path']; 106 | if (!empty($fileConfig['namespace'])) { 107 | $fileNamespace = Helper::replaceCurrentAppTemplate($fileConfig['namespace'], $currentApps); 108 | $fileNamespace = Helper::replaceTemplate($fileNamespace, $params['form'],"form."); 109 | } else { 110 | $fileNamespace = $filePath; 111 | } 112 | $fileNamespaceEndStr = substr($fileNamespace, -1); 113 | if ($fileNamespaceEndStr == '\\') { 114 | $fileNamespace = substr($fileNamespace, 0, strlen($fileNamespace) - 1); 115 | } 116 | $template = Helper::replaceCurrentAppTemplate($fileConfig['template'], $currentApps); 117 | $template = Helper::replaceTemplate($template, $params['form'],"form."); 118 | $tplParams[$file['name']] = [ 119 | 'class_name' => $file['value'], 120 | 'path' => $filePath, 121 | 'namespace' => $fileNamespace, 122 | 'template' => $template 123 | ]; 124 | 125 | // 验证模板是否存在 126 | $templatePath =DirAndFile::formatPath( APIDOC_ROOT_PATH . $template,"/"); 127 | if (is_readable($templatePath) == false) { 128 | throw new ErrorException("template not found", [ 129 | 'template' => $template 130 | ]); 131 | } 132 | // 验证是否已存在生成的文件 133 | $fileFullPath = DirAndFile::formatPath(APIDOC_ROOT_PATH . $filePath, "/"); 134 | $type = "folder"; 135 | if (strpos($fileFullPath, '.php') !== false) { 136 | // 路径为php文件,则验证文件是否存在 137 | if (is_readable($fileFullPath) == false) { 138 | throw new ErrorException("file not exists", [ 139 | 'filepath' => $filePath 140 | ]); 141 | } 142 | $type = "file"; 143 | } else { 144 | $fileName = !empty($file['value']) ? $file['value'] : ""; 145 | $fileFullPath = $fileFullPath . "/" . $fileName . ".php"; 146 | if (is_readable($fileFullPath)) { 147 | throw new ErrorException("file already exists",[ 148 | 'filepath' => DirAndFile::formatPath($filePath) . $fileName . ".php" 149 | ]); 150 | } 151 | } 152 | $createFiles[] = [ 153 | 'fileFullPath' => $fileFullPath, 154 | 'template' => $template, 155 | 'templatePath'=>$templatePath, 156 | 'type' => $type 157 | ]; 158 | } 159 | } 160 | 161 | $createModels = $this->checkModels($generatorItem,$tplParams,$currentApps,$params); 162 | $tplParams['tables'] = $createModels['tables']; 163 | return [ 164 | 'tplParams'=>$tplParams, 165 | 'createFiles'=>$createFiles, 166 | 'createModels' =>$createModels['createModels'] 167 | ]; 168 | } 169 | 170 | /** 171 | * 验证模型及表 172 | * @param $generatorItem 173 | * @param $tplParams 174 | * @return array 175 | */ 176 | protected function checkModels($generatorItem,$tplParams,$currentApps,$params=[]){ 177 | if (empty($this->config['database_query_function'])){ 178 | throw new ErrorException("not datatable_query_function config"); 179 | } 180 | 181 | $res=""; 182 | $tabls = $tplParams['tables']; 183 | $newTables = []; 184 | $createModels = []; 185 | if (!empty($tabls) && count($tabls)){ 186 | foreach ($tabls as $k=>$table) { 187 | $tableConfig = $generatorItem['table']; 188 | $fileFullPath=""; 189 | if (!empty($table['model_name'])){ 190 | $namespace = Helper::replaceCurrentAppTemplate($tableConfig['items'][$k]['namespace'], $currentApps); 191 | $namespace = Helper::replaceTemplate($namespace, $params['form'],"form."); 192 | $path = Helper::replaceCurrentAppTemplate($tableConfig['items'][$k]['path'], $currentApps); 193 | $path = Helper::replaceTemplate($path, $params['form'],"form."); 194 | $template = $tableConfig['items'][$k]['template']; 195 | 196 | // 验证模板是否存在 197 | $templatePath = DirAndFile::formatPath(APIDOC_ROOT_PATH . $template,"/"); 198 | if (is_readable($templatePath) == false) { 199 | throw new ErrorException("template not found", [ 200 | 'template' => $template 201 | ]); 202 | } 203 | $tplParams['tables'][$k]['class_name'] =$table['model_name']; 204 | // 验证模型是否已存在 205 | $fileName = $table['model_name']; 206 | $fileFullPath = DirAndFile::formatPath(APIDOC_ROOT_PATH.$path). "/" . $fileName . ".php"; 207 | if (is_readable($fileFullPath)) { 208 | throw new ErrorException("file already exists", [ 209 | 'filepath' => DirAndFile::formatPath($path) . "/" . $fileName . ".php" 210 | ]); 211 | } 212 | } 213 | // 验证表是否存在 214 | if ($table['table_name']){ 215 | $table_name = $this->databaseConfig['prefix'].$table['table_name']; 216 | $isTable = $this->config['database_query_function']('SHOW TABLES LIKE '."'".$table_name."'"); 217 | if ($isTable){ 218 | throw new ErrorException("datatable already exists", [ 219 | 'table' => $table_name 220 | ]); 221 | } 222 | } 223 | $table['namespace']=$namespace; 224 | $table['path']=$path; 225 | $table['model_path']=$path; 226 | $newTables[]=$table; 227 | $createModels[]=[ 228 | 'namespace'=>$namespace, 229 | 'template'=>$template, 230 | 'path'=>$path, 231 | 'templatePath' =>$templatePath, 232 | 'table'=>$table, 233 | 'fileFullPath'=>$fileFullPath 234 | ]; 235 | } 236 | } 237 | return ['createModels'=>$createModels,'tables'=>$newTables]; 238 | 239 | } 240 | 241 | /** 242 | * 创建文件 243 | * @param $createFiles 244 | * @param $tplParams 245 | * @return mixed 246 | */ 247 | protected function createFiles($createFiles,$tplParams){ 248 | 249 | if (!empty($createFiles) && count($createFiles)>0){ 250 | foreach ($createFiles as $fileItem) { 251 | $html = (new ParseTemplate())->compile($fileItem['templatePath'],$tplParams); 252 | if ($fileItem['type'] === "file"){ 253 | // 路径为文件,则添加到该文件 254 | $pathFileContent = DirAndFile::getFileContent($fileItem['fileFullPath']); 255 | $content = $pathFileContent."\r\n".$html; 256 | DirAndFile::createFile($fileItem['fileFullPath'],$content); 257 | }else{ 258 | DirAndFile::createFile($fileItem['fileFullPath'],$html); 259 | } 260 | } 261 | } 262 | return $tplParams; 263 | } 264 | 265 | /** 266 | * 创建模型文件 267 | * @param $createModels 268 | * @param $tplParams 269 | */ 270 | protected function createModels($createModels,$tplParams){ 271 | if (!empty($createModels) && count($createModels)>0){ 272 | foreach ($createModels as $k=>$item) { 273 | $table = $item['table']; 274 | if (!empty($table['table_name'])){ 275 | $res = $this->createTable($table); 276 | } 277 | if (!empty($table['model_name'])){ 278 | $tplParams['tables'][$k]['class_name'] =$table['model_name']; 279 | $html = (new ParseTemplate())->compile($item['templatePath'],$tplParams); 280 | DirAndFile::createFile($item['fileFullPath'],$html); 281 | } 282 | 283 | } 284 | } 285 | } 286 | 287 | /** 288 | * 创建数据表 289 | * @return mixed 290 | */ 291 | protected function createTable($table){ 292 | $datas = $table['datas']; 293 | $comment= ""; 294 | if (!empty($table['table_comment'])){ 295 | $comment =$table['table_comment']; 296 | } 297 | $table_name = $this->databaseConfig['prefix'].$table['table_name']; 298 | $table_data = ''; 299 | $main_keys = ''; 300 | $defaultNullTypes = ['timestamp']; 301 | foreach ($datas as $k=>$item){ 302 | if (!empty($item['not_table_field'])){ 303 | continue; 304 | } 305 | $table_field="`".$item['field']."` ".$item['type']; 306 | if (!empty($item['length'])){ 307 | $table_field.="(".$item['length'].")"; 308 | } 309 | 310 | if (!empty($item['main_key'])){ 311 | $main_keys.=$item['field']; 312 | $table_field.=" NOT NULL"; 313 | }else if (!empty($item['not_null'])){ 314 | $table_field.=" NOT NULL"; 315 | } 316 | if (!empty($item['incremental']) && !empty($item['main_key'])){ 317 | $table_field.=" AUTO_INCREMENT"; 318 | } 319 | if (!empty($item['default']) || (isset($item['default']) && $item['default']=="0")){ 320 | $defaultValue = "'".$item['default']."'"; 321 | if (in_array($item['default'],$this->systemDefaultValues)){ 322 | $defaultValue = $item['default']; 323 | } 324 | $table_field.=" DEFAULT ".$defaultValue.""; 325 | }else if (!empty($item['main_key']) && !$item['not_null']){ 326 | $table_field.=" DEFAULT NULL"; 327 | }else if (in_array($item['type'],$defaultNullTypes) && empty($item['not_null'])){ 328 | $table_field.=" NULL DEFAULT NULL"; 329 | } 330 | $fh = $k < (count($datas)-1)?",":""; 331 | $table_field.=" COMMENT '".$item['desc']."'".$fh; 332 | $table_data.=$table_field; 333 | } 334 | $primaryKey = ""; 335 | if (!empty($main_keys)){ 336 | $table_data.=","; 337 | $primaryKey = "PRIMARY KEY (`$main_keys`)"; 338 | } 339 | 340 | $charset = $this->databaseConfig['charset']; 341 | $engine = $this->databaseConfig['engine']; 342 | $sql = "CREATE TABLE IF NOT EXISTS `$table_name` ( 343 | $table_data 344 | $primaryKey 345 | ) ENGINE=$engine DEFAULT CHARSET=$charset COMMENT='$comment' AUTO_INCREMENT=1 ;"; 346 | 347 | try { 348 | $this->config['database_query_function']($sql); 349 | return true; 350 | } catch (\Exception $e) { 351 | throw new ErrorException("datatable create error", [ 352 | 'table' => $table_name, 353 | 'message'=>$e->getMessage(), 354 | 'sql'=>$sql 355 | ]); 356 | } 357 | } 358 | } -------------------------------------------------------------------------------- /src/Controller.php: -------------------------------------------------------------------------------- 1 | config = ConfigProvider::get(); 32 | if (isset($this->config['enable']) && $this->config['enable'] === false) { 33 | throw new ErrorException("apidoc close"); 34 | } 35 | if (!empty($this->config['request_params'])) { 36 | $this->requestParams = $this->config['request_params']; 37 | } else { 38 | $this->requestParams = (new Request())->param(); 39 | } 40 | if (!empty($this->requestParams['lang']) && !empty($this->config['lang_register_function'])) { 41 | $this->lang = $this->requestParams['lang']; 42 | $this->config['lang_register_function']($this->lang); 43 | } 44 | if ($checkAuth) { 45 | (new Auth($this->config))->checkAuth($this->requestParams); 46 | } 47 | } 48 | 49 | /** 50 | * 验证权限 51 | * @param $config 52 | * @param $params 53 | */ 54 | protected function checkAuth() 55 | { 56 | $config = $this->config; 57 | $params = $this->requestParams; 58 | if (!empty($params['shareKey'])) { 59 | //分享 60 | $shareData = (new ApiShare())->checkShareAuth($config, $params); 61 | $appKey = !empty($params['appKey']) ? $params['appKey'] : ""; 62 | if (!empty($shareData['appKeys']) && !in_array($appKey, $shareData['appKeys'])) { 63 | throw new ErrorException("share not exists"); 64 | } 65 | return $shareData; 66 | } else { 67 | (new Auth($config))->checkAuth($params); 68 | } 69 | } 70 | 71 | 72 | /** 73 | * 获取配置 74 | * @return \think\response\Json 75 | */ 76 | public function getConfig() 77 | { 78 | $this->init(false); 79 | $params = $this->requestParams; 80 | if (!empty($params['shareKey'])) { 81 | // 接口分享 82 | $shareData = (new ApiShare())->checkShareAuth($this->config, $params); 83 | if (!empty($shareData['appKeys'])) { 84 | $config = ConfigProvider::getFeConfig($shareData['appKeys']); 85 | } else { 86 | $config = ConfigProvider::getFeConfig(); 87 | } 88 | } else { 89 | (new Auth($this->config))->checkAuth($params); 90 | $config = ConfigProvider::getFeConfig(); 91 | } 92 | return Helper::showJson(0, "", $config); 93 | } 94 | 95 | /** 96 | * 验证密码 97 | */ 98 | public function verifyAuth() 99 | { 100 | $this->init(); 101 | $config = $this->config; 102 | $params = $this->requestParams; 103 | if (empty($params['password'])) { 104 | throw new ErrorException("password not found"); 105 | } 106 | $appKey = !empty($params['appKey']) ? $params['appKey'] : ""; 107 | if (!empty($params['shareKey'])) { 108 | // 接口分享 109 | $shareData = (new ApiShare())->getShareDetailByKey($params['shareKey']); 110 | if (!empty($shareData['password']) && $params['password'] === md5($shareData['password'])) { 111 | $hasAuth = (new Auth($config))->createToken($params['password']); 112 | } else { 113 | throw new ErrorException("password error"); 114 | } 115 | } else { 116 | if (!$appKey && !(!empty($config['auth']) && $config['auth']['enable'])) { 117 | throw new ErrorException("password error"); 118 | } 119 | $hasAuth = (new Auth($config))->verifyAuth($params['password'], $appKey); 120 | } 121 | $res = [ 122 | "token" => $hasAuth 123 | ]; 124 | return Helper::showJson(0, "", $res); 125 | } 126 | 127 | /** 128 | * 获取api文档菜单 129 | */ 130 | public function getApiMenus() 131 | { 132 | $this->init(false); 133 | $config = $this->config; 134 | $params = $this->requestParams; 135 | if (empty($params['appKey'])) { 136 | throw new ErrorException("appkey not found"); 137 | } 138 | $appKey = $params['appKey']; 139 | $shareData = $this->checkAuth(); 140 | $currentAppConfig = Helper::getCurrentAppConfig($appKey); 141 | $currentApp = $currentAppConfig['appConfig']; 142 | $apiData = $this->getApiMenusByAppKey($appKey); 143 | $groups = !empty($currentApp['groups']) ? $currentApp['groups'] : []; 144 | if (!empty($params['shareKey']) && $shareData['type'] == 'api') { 145 | $apiData['data'] = Helper::filterTreeNodesByKeys($apiData['data'], $shareData['apiKeys'], 'menuKey'); 146 | } 147 | $json = [ 148 | 'data' => $apiData['data'], 149 | 'app' => $currentApp, 150 | 'groups' => $groups, 151 | 'tags' => $apiData['tags'], 152 | ]; 153 | return Helper::showJson(0, "", $json); 154 | } 155 | 156 | protected function getApiMenusByAppKey($appKey) 157 | { 158 | $config = $this->config; 159 | if (!empty($config['cache']) && $config['cache']['enable']) { 160 | $cacheKey = Helper::getCacheKey('apiMenu', $appKey, $this->lang); 161 | $cacheData = (new Cache())->get($cacheKey); 162 | if ($cacheData && empty($params['reload'])) { 163 | $apiData = $cacheData; 164 | } else { 165 | // 生成数据并缓存 166 | $apiData = (new ParseApiMenus($config))->renderApiMenus($appKey); 167 | (new Cache())->set($cacheKey, $apiData); 168 | } 169 | } else { 170 | // 生成数据 171 | $apiData = (new ParseApiMenus($config))->renderApiMenus($appKey); 172 | } 173 | return $apiData; 174 | } 175 | 176 | 177 | /** 178 | * 获取所有api文档菜单 179 | */ 180 | public function getAllApiMenus() 181 | { 182 | $this->init(true); 183 | $config = $this->config; 184 | $params = $this->requestParams; 185 | $configApps = Helper::handleAppsConfig($config['apps'], false, $config); 186 | $data = ApiShare::getAppShareApis($config, $configApps, "", [], false); 187 | return Helper::showJson(0, "", $data); 188 | } 189 | 190 | /** 191 | * 获取接口明细 192 | * @return array 193 | */ 194 | public function getApiDetail() 195 | { 196 | $this->init(false); 197 | $config = $this->config; 198 | $params = $this->requestParams; 199 | if (empty($params['path'])) { 200 | throw new ErrorException("path not found"); 201 | } 202 | $appKey = !empty($params['appKey']) ? $params['appKey'] : ""; 203 | $apiKey = urldecode($params['path']); 204 | $this->checkAuth(); 205 | if (!empty($config['cache']) && $config['cache']['enable']) { 206 | $cacheKey = Helper::getCacheKey('apiDetail', $appKey, $this->lang, $params['path']); 207 | $cacheData = (new Cache())->get($cacheKey); 208 | if ($cacheData && empty($params['reload'])) { 209 | $res = $cacheData; 210 | } else { 211 | // 生成数据并缓存 212 | $res = (new ParseApiDetail($config))->renderApiDetail($appKey, $apiKey); 213 | (new Cache())->set($cacheKey, $res); 214 | } 215 | } else { 216 | // 生成数据 217 | $res = (new ParseApiDetail($config))->renderApiDetail($appKey, $apiKey); 218 | } 219 | $res['appKey'] = $appKey; 220 | return Helper::showJson(0, "", $res); 221 | } 222 | 223 | 224 | /** 225 | * 获取md菜单 226 | * @return array 227 | */ 228 | public function getMdMenus() 229 | { 230 | $this->init(false); 231 | $config = $this->config; 232 | $params = $this->requestParams; 233 | $appKey = ""; 234 | if (!empty($params['appKey'])) { 235 | // 获取指定应用 236 | $appKey = $params['appKey']; 237 | } 238 | $this->checkAuth(); 239 | $docs = (new ParseMarkdown($config))->getDocsMenu($appKey, $this->lang); 240 | return Helper::showJson(0, "", $docs); 241 | 242 | } 243 | 244 | /** 245 | * 获取md文档内容 246 | * @return \think\response\Json 247 | */ 248 | public function getMdDetail() 249 | { 250 | $this->init(false); 251 | $config = $this->config; 252 | $params = $this->requestParams; 253 | $this->checkAuth(); 254 | try { 255 | if (empty($params['path'])) { 256 | throw new ErrorException("mdPath not found"); 257 | } 258 | if (empty($params['appKey'])) { 259 | throw new ErrorException("appkey not found"); 260 | } 261 | 262 | $path = urldecode($params['path']); 263 | $content = (new ParseMarkdown($config))->getContent($params['appKey'], $path, $this->lang); 264 | $res = [ 265 | 'content' => $content, 266 | ]; 267 | return Helper::showJson(0, "", $res); 268 | 269 | } catch (ErrorException $e) { 270 | return Helper::showJson($e->getCode(), $e->getMessage()); 271 | } 272 | } 273 | 274 | 275 | /** 276 | * 创建代码生成器 277 | * @return array 278 | */ 279 | public function createGenerator() 280 | { 281 | $this->init(true); 282 | $config = $this->config; 283 | $params = $this->requestParams; 284 | $res = (new generator\Index($config))->create($params); 285 | return Helper::showJson(0, "", $res); 286 | } 287 | 288 | /** 289 | * 删除所有接口缓存 290 | * @return array 291 | */ 292 | public function cancelAllCache() 293 | { 294 | $this->init(true); 295 | $config = $this->config; 296 | $path = APIDOC_STORAGE_PATH . $config['cache']['folder']. '/apis'; 297 | $res = DirAndFile::deleteDir($path); 298 | return Helper::showJson(0, "", $path); 299 | } 300 | 301 | /** 302 | * 生成所有接口缓存 303 | * @return array 304 | */ 305 | public function createAllCache() 306 | { 307 | $this->init(true); 308 | $config = $this->config; 309 | $params = $this->requestParams; 310 | $apps = Helper::getAllApps($config['apps']); 311 | $cache = new Cache(); 312 | DirAndFile::deleteDir(APIDOC_STORAGE_PATH . $config['cache']['folder'] . '/' . 'apis'); 313 | if (!empty($apps) && count($apps)) { 314 | try { 315 | foreach ($apps as $app) { 316 | // 缓存菜单 317 | $appKey = $app['appKey']; 318 | $controllerData = (new ParseApiMenus($config))->renderApiMenus($appKey); 319 | if (!empty($controllerData['data']) && count($controllerData['data'])) { 320 | foreach ($controllerData['data'] as $controller) { 321 | if (!empty($controller['children']) && count($controller['children'])) { 322 | foreach ($controller['children'] as $item) { 323 | if (!empty($item['url']) && !empty($item['menuKey'])) { 324 | $apiDetail = (new ParseApiDetail($config))->renderApiDetail($appKey, urldecode($item['menuKey'])); 325 | $apiDetailCacheKey = Helper::getCacheKey('apiDetail', $appKey, $this->lang, $item['menuKey']); 326 | $cache->set($apiDetailCacheKey, $apiDetail); 327 | } 328 | } 329 | } 330 | } 331 | } 332 | $cacheKey = Helper::getCacheKey('apiMenu', $appKey, $this->lang); 333 | $cache->set($cacheKey, $controllerData); 334 | } 335 | } catch (\ReflectionException $e) { 336 | DirAndFile::deleteDir(APIDOC_STORAGE_PATH . $config['cache']['folder'] . '/' . 'apis'); 337 | throw new ErrorException($e->getMessage()); 338 | } 339 | } 340 | return Helper::showJson(0, "", true); 341 | } 342 | 343 | /** 344 | * 生成代码模板内容 345 | * @return array 346 | */ 347 | public function renderCodeTemplate() 348 | { 349 | $this->init(true); 350 | $config = $this->config; 351 | $params = $this->requestParams; 352 | $code = (new ParseCodeTemplate($config))->renderCode($params); 353 | return Helper::showJson(0, "", [ 354 | 'code' => $code 355 | ]); 356 | 357 | } 358 | 359 | 360 | /** 361 | * 添加接口分享 362 | * @return array 363 | */ 364 | public function addApiShare() 365 | { 366 | $this->init(true); 367 | $config = $this->config; 368 | $params = $this->requestParams; 369 | if (empty($params['name'])) { 370 | throw new ErrorException('field not found', ['field' => 'name']); 371 | } 372 | if (empty($params['type'])) { 373 | throw new ErrorException('field not found', ['field' => 'type']); 374 | } 375 | if ($params['type'] == 'app' && empty($params['appKeys'])) { 376 | throw new ErrorException('field not found', ['field' => 'appKeys']); 377 | } 378 | if ($params['type'] == 'api' && empty($params['apiKeys'])) { 379 | throw new ErrorException('field not found', ['field' => 'apiKeys']); 380 | } 381 | $res = (new ApiShare())->addApiShare($params); 382 | return Helper::showJson(0, "", $res); 383 | } 384 | 385 | /** 386 | * 获取接口分享分页列表 387 | * @return array 388 | */ 389 | public function getApiShareList() 390 | { 391 | $this->init(true); 392 | $config = $this->config; 393 | $params = $this->requestParams; 394 | $pageIndex = !empty($params['pageIndex']) ? $params['pageIndex'] : 1; 395 | $pageSize = 5; 396 | $res = (new ApiShare())->getSharePageList($config, $pageIndex, $pageSize); 397 | return Helper::showJson(0, "", $res); 398 | } 399 | 400 | /** 401 | * 获取接口分享记录明细 402 | * @return array 403 | */ 404 | public function getApiShareDetail() 405 | { 406 | $this->init(true); 407 | $config = $this->config; 408 | $params = $this->requestParams; 409 | if (empty($params['key'])) { 410 | throw new ErrorException('field not found', ['field' => 'key']); 411 | } 412 | $cacheData = (new ApiShare())->getShareDetailByKey($params['key']); 413 | return Helper::showJson(0, "", $cacheData); 414 | } 415 | 416 | /** 417 | * 删除接口分享记录 418 | * @return array 419 | */ 420 | public function deleteApiShare() 421 | { 422 | $this->init(true); 423 | $config = $this->config; 424 | $params = $this->requestParams; 425 | if (empty($params['key'])) { 426 | throw new ErrorException('field not found', ['field' => 'key']); 427 | } 428 | $cacheKey = ApiShare::getShareCacheKey($params['key']); 429 | $res = (new Cache())->delete($cacheKey); 430 | return Helper::showJson(0, "", $res); 431 | } 432 | 433 | /** 434 | * 处理接口分享操作 435 | * @return array 436 | */ 437 | public function handleApiShareAction() 438 | { 439 | $this->init(true); 440 | $config = $this->config; 441 | $params = $this->requestParams; 442 | if (empty($params['key'])) { 443 | throw new ErrorException('field not found', ['field' => 'key']); 444 | } 445 | if (!isset($params['index'])) { 446 | throw new ErrorException('field not found', ['field' => 'index']); 447 | } 448 | $res = (new ApiShare())->handleApiShareAction($config, $params['key'], $params['index']); 449 | return Helper::showJson(0, "", $res); 450 | } 451 | 452 | /** 453 | * 导出swagger.json 454 | * @return array 455 | */ 456 | public function exportSwagger() 457 | { 458 | 459 | $this->init(true); 460 | $config = $this->config; 461 | $params = $this->requestParams; 462 | if($config['export_config']['enable'] === false){ 463 | throw new ErrorException('export config not enable'); 464 | } 465 | if (empty($params['key'])) { 466 | throw new ErrorException('field not found', ['field' => 'key']); 467 | } 468 | $searchData = (new ApiShare())->getShareData($config,$params['key']); 469 | 470 | $res = (new ExportSwagger($config['export_config']))->exportJson($config,$searchData); 471 | return Helper::showJson(0, "", $res); 472 | } 473 | 474 | } 475 | --------------------------------------------------------------------------------