├── .gitattributes ├── .gitignore ├── .php_cs ├── LICENSE ├── README.md ├── README_OLD.md ├── composer.json ├── phpstan.neon ├── phpunit.xml ├── publish └── apidog.php ├── src ├── Annotation │ ├── ApiController.php │ ├── ApiDefinition.php │ ├── ApiDefinitions.php │ ├── ApiResponse.php │ ├── ApiServer.php │ ├── ApiVersion.php │ ├── Body.php │ ├── DeleteApi.php │ ├── FormData.php │ ├── GetApi.php │ ├── Header.php │ ├── Param.php │ ├── Path.php │ ├── PostApi.php │ ├── PutApi.php │ └── Query.php ├── ApiAnnotation.php ├── BootAppConfListener.php ├── ConfigProvider.php ├── DispatcherFactory.php ├── Exception │ └── ApiDogException.php ├── Middleware │ └── ApiValidationMiddleware.php ├── Swagger │ └── SwaggerJson.php ├── UICommand.php ├── Validation │ ├── Validation.php │ ├── ValidationApi.php │ └── ValidationCustomRule.php └── function.php └── ui └── default ├── favicon-16x16.png ├── favicon-32x32.png ├── index_tpl.html ├── oauth2-redirect.html ├── swagger-ui-bundle.js ├── swagger-ui-standalone-preset.js ├── swagger-ui.css └── swagger-ui.js /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | /.github export-ignore 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | *.cache 4 | *.log 5 | 6 | # Mac DS_Store Files 7 | .DS_Store 8 | 9 | # phpstorm project files 10 | .idea 11 | ui/default/*.json 12 | ui/default/index.html 13 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | '@Symfony' => true, 17 | '@DoctrineAnnotation' => true, 18 | '@PhpCsFixer' => true, 19 | 'header_comment' => [ 20 | 'commentType' => 'PHPDoc', 21 | 'header' => $header, 22 | 'separate' => 'none', 23 | 'location' => 'after_declare_strict', 24 | ], 25 | 'array_syntax' => [ 26 | 'syntax' => 'short' 27 | ], 28 | 'list_syntax' => [ 29 | 'syntax' => 'short' 30 | ], 31 | 'concat_space' => [ 32 | 'spacing' => 'one' 33 | ], 34 | 'blank_line_before_statement' => [ 35 | 'statements' => [ 36 | 'declare', 37 | ], 38 | ], 39 | 'general_phpdoc_annotation_remove' => [ 40 | 'annotations' => [ 41 | 'author' 42 | ], 43 | ], 44 | 'ordered_imports' => [ 45 | 'imports_order' => [ 46 | 'class', 'function', 'const', 47 | ], 48 | 'sort_algorithm' => 'alpha', 49 | ], 50 | 'single_line_comment_style' => [ 51 | 'comment_types' => [ 52 | ], 53 | ], 54 | 'yoda_style' => [ 55 | 'always_move_variable' => false, 56 | 'equal' => false, 57 | 'identical' => false, 58 | ], 59 | 'phpdoc_align' => [ 60 | 'align' => 'left', 61 | ], 62 | 'multiline_whitespace_before_semicolons' => [ 63 | 'strategy' => 'no_multi_line', 64 | ], 65 | 'constant_case' => [ 66 | 'case' => 'lower', 67 | ], 68 | 'class_attributes_separation' => true, 69 | 'combine_consecutive_unsets' => true, 70 | 'declare_strict_types' => true, 71 | 'linebreak_after_opening_tag' => true, 72 | 'lowercase_static_reference' => true, 73 | 'no_useless_else' => true, 74 | 'no_unused_imports' => true, 75 | 'not_operator_with_successor_space' => true, 76 | 'not_operator_with_space' => false, 77 | 'ordered_class_elements' => true, 78 | 'php_unit_strict' => false, 79 | 'phpdoc_separation' => false, 80 | 'single_quote' => true, 81 | 'standardize_not_equals' => true, 82 | 'multiline_comment_opening_closing' => true, 83 | ]) 84 | ->setFinder( 85 | PhpCsFixer\Finder::create() 86 | ->exclude('bin') 87 | ->exclude('public') 88 | ->exclude('runtime') 89 | ->exclude('vendor') 90 | ->in(__DIR__) 91 | ) 92 | ->setUsingCache(false); 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 刀刀 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Api Watch Dog 2 | 一个 [Hyperf](https://github.com/hyperf/hyperf) 框架的 Api 参数校验及 swagger 文档生成组件 3 | 4 | 1. 根据注解自动进行Api参数的校验, 业务代码更纯粹. 5 | 2. 根据注解自动生成Swagger文档, 让接口文档维护更省心. 6 | 7 | > 在 1.2 版本后, 本扩展移除了内部自定义的验证器, 只保留的 hyperf 原生验证器, 以保持验证规则的统一 8 | 9 | 旧版本文档 [查看](./README_OLD.md) 10 | 11 | ## 安装 12 | 13 | ``` 14 | composer require daodao97/apidog 15 | ``` 16 | ## 使用 17 | 18 | #### 1. 发布配置文件 19 | 20 | ```bash 21 | php bin/hyperf.php vendor:publish daodao97/apidog 22 | 23 | # hyperf/validation 的依赖发布 24 | 25 | php bin/hyperf.php vendor:publish hyperf/translation 26 | 27 | php bin/hyperf.php vendor:publish hyperf/validation 28 | ``` 29 | 30 | ### 2. 修改配置文件 31 | 32 | > 注意 与1.2及之前的版本相比, 配置文件结构及文件名 略有不同 33 | > 34 | > (1) 配置文件结构的优化, 增加了swagger外的整体配置 35 | > 36 | > (2) 配置文件的名称 由 swagger.php 改为 apidog.php 37 | 38 | 根据需求修改 `config/autoload/apidog.php` 39 | 40 | ```php 41 | env('APP_ENV') !== 'production', 46 | // swagger 配置的输出文件 47 | // 当你有多个 http server 时, 可以在输出文件的名称中增加 {server} 字面变量 48 | // 比如 /public/swagger/swagger_{server}.json 49 | 'output_file' => BASE_PATH . '/public/swagger/swagger.json', 50 | // 忽略的hook, 非必须 用于忽略符合条件的接口, 将不会输出到上定义的文件中 51 | 'ignore' => function ($controller, $action) { 52 | return false; 53 | }, 54 | // 自定义验证器错误码、错误描述字段 55 | 'error_code' => 400, 56 | 'http_status_code' => 400, 57 | 'field_error_code' => 'code', 58 | 'field_error_message' => 'message', 59 | 'exception_enable' => false, 60 | // swagger 的基础配置 61 | 'swagger' => [ 62 | 'swagger' => '2.0', 63 | 'info' => [ 64 | 'description' => 'hyperf swagger api desc', 65 | 'version' => '1.0.0', 66 | 'title' => 'HYPERF API DOC', 67 | ], 68 | 'host' => 'apidog.cc', 69 | 'schemes' => ['http'], 70 | ], 71 | 'templates' => [ 72 | // {template} 字面变量 替换 schema 内容 73 | // // 默认 成功 返回 74 | // 'success' => [ 75 | // "code|code" => '0', 76 | // "result" => '{template}', 77 | // "message|message" => 'Success', 78 | // ], 79 | // // 分页 80 | // 'page' => [ 81 | // "code|code" => '0', 82 | // "result" => [ 83 | // 'pageSize' => 10, 84 | // 'total' => 1, 85 | // 'totalPage' => 1, 86 | // 'list' => '{template}' 87 | // ], 88 | // "message|message" => 'Success', 89 | //], 90 | ], 91 | // golbal 节点 为全局性的 参数配置 92 | // 跟注解相同, 支持 header, path, query, body, formData 93 | // 子项为具体定义 94 | // 模式一: [ key => rule ] 95 | // 模式二: [ [key, rule, defautl, description] ] 96 | 'global' => [ 97 | // 'header' => [ 98 | // "x-token|验签" => "required|cb_token" 99 | // ], 100 | // 'query' => [ 101 | // [ 102 | // 'key' => 'xx|cc', 103 | // 'rule' => 'required', 104 | // 'default' => 'abc', 105 | // 'description' => 'description' 106 | // ] 107 | // ] 108 | ] 109 | ]; 110 | ``` 111 | 112 | ### 3. 启用 Api参数校验中间件 113 | 114 | ```php 115 | // config/autoload/middlewares.php 116 | 117 | Hyperf\Apidog\Middleware\ApiValidationMiddleware::class 118 | ``` 119 | 120 | ### 4. 校验规则的定义 121 | 122 | 规则列表参见 [hyperf/validation 文档](https://hyperf.wiki/#/zh-cn/validation?id=%e9%aa%8c%e8%af%81%e8%a7%84%e5%88%99) 123 | 124 | 更详细的规则支持列表可以参考 [laravel/validation 文档](https://learnku.com/docs/laravel/6.x/validation/5144#c58a91) 125 | 126 | 扩展在原生的基础上进行了封装, 支持方便的进行 `自定义校验` 和 `控制器回调校验` 127 | 128 | ## 实现思路 129 | 130 | api参数的自动校验: 通过中间件拦截 http 请求, 根据注解中的参数定义, 通过 `valiation` 自动验证和过滤, 如果验证失败, 则拦截请求. 其中`valiation` 包含 规则校验, 参数过滤, 自定义校验 三部分. 131 | 132 | swagger文档生成: 在`php bin/hyperf.php start` 启动 `http-server` 时, 通过监听 `BootAppConfListener` 事件, 扫码控制器注解, 通过注解中的 访问类型, 参数格式, 返回类型 等, 自动组装 `swagger.json` 结构, 最后输出到 `config/autoload/apidog.php` 定义的文件路径中 133 | 134 | ## 支持的注解 135 | 136 | #### Api类型 137 | `GetApi`, `PostApi`, `PutApi`, `DeleteApi` 138 | 139 | ### 参数类型 140 | `Header`, `Query`, `Body`, `FormData`, `Path` 141 | 142 | ### 其他 143 | `ApiController`, `ApiResponse`, `ApiVersion`, `ApiServer`, `ApiDefinitions`, `ApiDefinition` 144 | 145 | ```php 146 | /** 147 | * @ApiVersion(version="v1") 148 | * @ApiServer(name="http") 149 | */ 150 | class UserController {} 151 | ``` 152 | 153 | `ApiServer` 当你在 `config/autoload.php/server.php servers` 中配置了多个 `http` 服务时, 如果想不同服务生成不同的`swagger.json` 可以在控制器中增加此注解. 154 | 155 | `ApiVersion` 当你的统一个接口存在不同版本时, 可以使用此注解, 路由注册时会为每个木有增加版本号, 如上方代码注册的实际路由为 `/v1/user/***` 156 | 157 | `ApiDefinition` 定义一个 `Definition`,用于Response的复用。 *swagger* 的difinition是以引用的方式来嵌套的,如果需要嵌套另外一个(值为object类型就需要嵌套了),可以指定具体 `properties` 中的 `$ref` 属性 158 | 159 | `ApiDefinitions` 定义一个组`Definition` 160 | 161 | `ApiResponse` 响应体的`schema`支持为key设置简介. `$ref` 属性可以引用 `ApiDefinition` 定义好的结构(该属性优先级最高) 162 | ```php 163 | @ApiResponse(code="0", description="删除成功", schema={"id|这里是ID":1}) 164 | @ApiResponse(code="0", description="删除成功", schema={"$ref": "ExampleResponse"}) 165 | ``` 166 | 167 | 具体使用方式参见下方样例 168 | 169 | ## 样例 170 | 171 | ```php 172 | 0, 226 | 'id' => 1, 227 | 'params' => $this->request->post(), 228 | ]; 229 | } 230 | 231 | // 自定义的校验方法 rule 中 cb_*** 方式调用 232 | public function checkName($attribute, $value) 233 | { 234 | if ($value === 'a') { 235 | return "拒绝添加 " . $value; 236 | } 237 | 238 | return true; 239 | } 240 | 241 | /** 242 | * 请注意 body 类型 rules 为数组类型 243 | * @DeleteApi(path="/demo", description="删除用户") 244 | * @Body(rules={ 245 | * "id|用户id":"required|integer|max:10", 246 | * "deepAssoc|深层关联":{ 247 | * "name_1|名称": "required|integer|max:20" 248 | * }, 249 | * "deepUassoc|深层索引":{{ 250 | * "name_2|名称": "required|integer|max:20" 251 | * }}, 252 | * "a.b.c.*.e|aa":"required|integer|max:10", 253 | * }) 254 | * @ApiResponse(code="-1", description="参数错误") 255 | * @ApiResponse(code="0", description="删除成功", schema={"id":1}) 256 | */ 257 | public function delete() 258 | { 259 | $body = $this->request->getBody()->getContents(); 260 | return [ 261 | 'code' => 0, 262 | 'query' => $this->request->getQueryParams(), 263 | 'body' => json_decode($body, true), 264 | ]; 265 | } 266 | 267 | /** 268 | * @GetApi(path="/demo", description="获取用户详情") 269 | * @Query(key="id", rule="required|integer|max:0") 270 | * @ApiResponse(code="-1", description="参数错误") 271 | * @ApiResponse(code="0", schema={"id":1,"name":"张三","age":1}, template="success") 272 | */ 273 | public function get() 274 | { 275 | return [ 276 | 'code' => 0, 277 | 'id' => 1, 278 | 'name' => '张三', 279 | 'age' => 1, 280 | ]; 281 | } 282 | 283 | /** 284 | * schema中可以指定$ref属性引用定义好的definition 285 | * @GetApi(path="/demo/info", description="获取用户详情") 286 | * @Query(key="id", rule="required|integer|max:0") 287 | * @ApiResponse(code="-1", description="参数错误") 288 | * @ApiResponse(code="0", schema={"$ref": "DemoOkResponse"}) 289 | */ 290 | public function info() 291 | { 292 | return [ 293 | 'code' => 0, 294 | 'id' => 1, 295 | 'name' => '张三', 296 | 'age' => 1, 297 | ]; 298 | } 299 | 300 | /** 301 | * @GetApi(path="/demos", summary="用户列表") 302 | * @ApiResponse(code="200", description="ok", schema={{ 303 | * "a|aa": {{ 304 | * "a|aaa":"b","c|ccc":5.2 305 | * }}, 306 | * "b|ids": {1,2,3}, 307 | * "c|strings": {"a","b","c"}, 308 | * "d|dd": {"a":"b","c":"d"}, 309 | * "e|ee": "f" 310 | * }}) 311 | */ 312 | public function list() 313 | { 314 | return [ 315 | [ 316 | "a" => [ 317 | ["a" => "b", "c" => "d"], 318 | ], 319 | "b" => [1, 2, 3], 320 | "c" => ["a", "b", "c"], 321 | "d" => [ 322 | "a" => "b", 323 | "c" => "d", 324 | ], 325 | "e" => "f", 326 | ], 327 | ]; 328 | } 329 | 330 | } 331 | ``` 332 | 333 | ## Swagger UI启动 334 | 335 | 本组件提供了两种方式来启用`SwaggerUI` 336 | , 当`config/autoload/apidog.php enable = true` 时 337 | 338 | #### 方式一 339 | 340 | 系统启动时, `swagger.json` 会自动输出到配置文件中定义的 `output_file`中, 此时我们到`swagger ui`的前端文件结合`nginx`启动web服务 341 | 342 | #### 方式二 343 | 344 | 也可以使用组件提供的快捷命令, 快速启动一个 `swagger ui`. 345 | 346 | ```bash 347 | php bin/hyperf.php apidog:ui 348 | 349 | php bin/hyperf.php apidog:ui --port 8888 350 | ``` 351 | 352 | ![hMvJnQ](https://gitee.com/daodao97/asset/raw/master/imgs/hMvJnQ.jpg) 353 | 354 | ## Swagger展示 355 | 356 | ![AOFVzI](https://gitee.com/daodao97/asset/raw/master/imgs/AOFVzI.jpg) 357 | 358 | ## 更新日志 359 | - 20220222 360 | - swagger bug fix by [PR](https://github.com/daodao97/apidog/pull/67) 361 | - 20210829 362 | - fix `swagger` 生成时 `server` 类型过滤问题, 屏蔽非http的服务 363 | - 增加 `global` 全局的参数规则, 详见 `apidog.php` `global` 节点 364 | - 20201230 365 | - 支持 hyperf 2.1 版本 366 | - 修复 `@Header` 参数名被底层转换为全小写导致的验证无效 367 | - 20201126 368 | - 统一 `version`, `prefix`, `path` 的前缀处理逻辑 [issue/42](https://github.com/daodao97/apidog/issues/42) 369 | - 20201111 [@ice](https://github.com/ice-leng) 370 | - 修复 初始化 swagger.json 文件生成 371 | - 修复 definition 在swagger ui 正确显示 定义数据类型 372 | - 添加 注解 Header ,Query 支持 类 注解 373 | - 添加 FormData 注解 key 参数 支持 a.b 验证 swagger ui 支持 374 | - 添加 Body 注解 支持 参数 a.b 和 a.*.b 验证 swagger ui 支持 375 | - 修复 definition 返回 参数为 小数在 swagger ui 不显示问题 376 | - 添加 异常 ApiDogException 抛出,以及配置 异常抛出开关 377 | - 添加 返回数据 模版 378 | - 20201014 379 | - 优化swagger ui, 命令模式监听`0.0.0.0`, 并支持自定义端口 380 | - 20200911 381 | - Response 增加纯列表模式 [@zxyfaxcn](https://github.com/zxyfaxcn) 382 | - 20200904 383 | - 增加 `ApiDefinitions` 与 `ApiDefinition` 注解,可用于相同Response结构体复用 [@jobinli](https://github.com/jobinli) 384 | - `ApiResponse schema` 增加 `$ref` 属性,用于指定由 `ApiDefinition` 定义的结构体 [@jobinli](https://github.com/jobinli) 385 | - 20200813 386 | - 增加Api版本, `ApiVersion`, 可以给路由增加版本前缀 387 | - 增加多服务支持, `ApiServer`, 可以按服务生成`swagger.json` 388 | - `ApiResponse shema` 支持字段简介 389 | - 20200812 390 | - `body` 结构增加多级支持 391 | - `FormData` 增加 文件上传样例 392 | - 增加`swagger ui`命令行工具 393 | -------------------------------------------------------------------------------- /README_OLD.md: -------------------------------------------------------------------------------- 1 | # hyperf apidog 2 | 3 | ## Api Watch Dog 4 | 一个 [Hyperf](https://github.com/hyperf-cloud/hyperf) 框架的 Api 参数校验及 swagger 文档生成扩展 5 | 6 | 1. 根据注解自动进行Api参数的校验, 业务代码更纯粹. 7 | 2. 根据注解自动生成Swagger文档, 让接口文档维护更省心. 8 | 9 | ## 安装 10 | 11 | ``` 12 | composer require daodao97/apidog:~1.1.0 13 | ``` 14 | 15 | ## 配置 16 | 17 | ```php 18 | // config/autoload/middlewares.php 定义使用中间件 19 | [ 24 | Hyperf\Apidog\Middleware\ApiValidationMiddleware::class, 25 | ], 26 | ]; 27 | 28 | // config/autoload/swagger.php swagger 基础信息 29 | BASE_PATH . '/public/swagger.json', 34 | 'swagger' => '2.0', 35 | 'info' => [ 36 | 'description' => 'hyperf swagger api desc', 37 | 'version' => '1.0.0', 38 | 'title' => 'HYPERF API DOC', 39 | ], 40 | 'host' => 'apidog.com', 41 | 'schemes' => ['http'] 42 | ]; 43 | 44 | // config/dependencies.php 重写 DispathcerFactory 依赖 45 | [ 50 | Hyperf\HttpServer\Router\DispatcherFactory::class => Hyperf\Apidog\DispatcherFactory::class 51 | ], 52 | ]; 53 | 54 | ``` 55 | 56 | ## 使用 57 | 58 | ```php 59 | 0, 91 | 'id' => 1 92 | ]; 93 | } 94 | 95 | /** 96 | * @DeleteApi(path="/user", description="删除用户") 97 | * @Body(rules={ 98 | * "id|用户id":"required|int|gt[0]" 99 | * }) 100 | * @ApiResponse(code="-1", description="参数错误") 101 | * @ApiResponse(code="0", description="删除成功", schema={"id":1}) 102 | */ 103 | public function delete() 104 | { 105 | return [ 106 | 'code' => 0, 107 | 'id' => 1 108 | ]; 109 | } 110 | 111 | /** 112 | * @GetApi(path="/user", description="获取用户详情") 113 | * @Query(key="id", rule="required|int|gt[0]") 114 | * @ApiResponse(code="-1", description="参数错误") 115 | * @ApiResponse(code="0", schema={"id":1,"name":"张三","age":1}) 116 | */ 117 | public function get() 118 | { 119 | return [ 120 | 'code' => 0, 121 | 'id' => 1, 122 | 'name' => '张三', 123 | 'age' => 1 124 | ]; 125 | } 126 | } 127 | ``` 128 | 129 | ## 实现思路 130 | 131 | api参数的自动校验: 通过中间件拦截 http 请求, 根据注解中的参数定义, 通过 `valiation` 自动验证和过滤, 如果验证失败, 则拦截请求. 其中`valiation` 包含 规则校验, 参数过滤, 自定义校验 三部分. 132 | 133 | swagger文档生成: 在`php bin/hyperf.php start` 启动http-server时, 系统会扫描所有控制器注解, 通过注解中的 访问类型, 参数格式, 返回类型 等, 自动组装swagger.json结构, 最后输出到 `config/autoload/swagger.php` 定义的文件路径中 134 | 135 | ### validation详解 136 | 137 | 1. 参数校验 `src/Validation.php`中定义的 rule_** 格式的方法名 138 | 139 | `any` 任意类型 140 | 141 | `required` 必填 142 | 143 | `uri` uri 格式 144 | 145 | `url` url 格式 146 | 147 | `email` 邮件格式 148 | 149 | `extended_json` 注释类型json字符串 150 | 151 | `json` json格式字符串 152 | 153 | `array` 数组 154 | 155 | `date` 2019-09-01 格式日志 156 | 157 | `datetime`, 158 | 159 | `safe_password` 安全密码 160 | 161 | `in` 在 *** 之中, 例如 `type|类型=required|int|in[1,2,3]` 162 | 163 | `max_width` 最大长度 164 | 165 | `min_width` 最小长度 166 | 167 | `natural`自然数 168 | 169 | `alpha` 字母 170 | 171 | `alpha_number` 数字字母 172 | 173 | `alhpa_dash`, 数字字母下划线 174 | 175 | `number` 数字 176 | 177 | `match`匹配, 参数中 key1 与 key2 校验相同时使用, 例如`key2=match[key1]` 178 | 179 | `mobile`手机号 180 | 181 | `gt` 大于 182 | 183 | `ge` 等于 184 | 185 | `lt` 小于 186 | 187 | `le` 小于等于 188 | 189 | `enum` 其中直接 `key=enum[1,2]` 类似`in` 190 | 191 | 2. 参数过滤 `src/Validation.php`中定义的 filter_** 格式的方法名 192 | 193 | `bool` 布尔过滤 194 | 195 | `int` int过滤 196 | 197 | 3. 控制器中定义的 自定义校验方法 例如rule为 `required|int|cb_customCheck`, 控制器中对应的 `checkCustom`方法, 将会自动调用 198 | 199 | ### swagger生成 200 | 201 | 1. api类型定义 `GetApi`, `PostApi`, `PutApi`, `DeleteApi` 202 | 2. 参数定义 `Header`, `Query`, `FormData`, `Body`, `Path` 203 | 3. 返回结果定义 `ApiResponse` 204 | 205 | ## Swagger展示 206 | 207 | ![swagger](http://tva1.sinaimg.cn/large/007X8olVly1g6j91o6xroj31k10u079l.jpg) 208 | 209 | ## TODO 210 | - 多层级参数的校验 211 | - swagger更多属性的支持 212 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daodao97/apidog", 3 | "description": "A swagger library for Hyperf.", 4 | "license": "MIT", 5 | "keywords": [ 6 | "php", 7 | "swoole", 8 | "hyperf", 9 | "swagger" 10 | ], 11 | "support": { 12 | }, 13 | "require": { 14 | "php": ">=8.0", 15 | "ext-json": "*", 16 | "hyperf/command": "3.0.*", 17 | "hyperf/http-server": "3.0.*", 18 | "hyperf/logger": "3.0.*", 19 | "hyperf/validation": "3.0.*" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "Hyperf\\Apidog\\": "src/" 24 | }, 25 | "files": [ 26 | "src/function.php" 27 | ] 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "HyperfTest\\Apidog\\": "tests/" 32 | } 33 | }, 34 | "minimum-stability": "dev", 35 | "prefer-stable": true, 36 | "config": { 37 | "sort-packages": true 38 | }, 39 | "extra": { 40 | "hyperf": { 41 | "config": "Hyperf\\Apidog\\ConfigProvider" 42 | } 43 | }, 44 | "require-dev": { 45 | "friendsofphp/php-cs-fixer": "^2.17", 46 | "mockery/mockery": "^1.0", 47 | "phpstan/phpstan": "^0.12", 48 | "phpunit/phpunit": ">=8.5" 49 | }, 50 | "scripts": { 51 | "cs-fix": "php-cs-fixer fix $1", 52 | "test": "phpunit -c phpunit.xml --colors=always", 53 | "analyse": "phpstan analyse --memory-limit 1024M -l 0 ./src" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | # Magic behaviour with __get, __set, __call and __callStatic is not exactly static analyser-friendly :) 2 | # Fortunately, You can ignore it by the following config. 3 | # 4 | 5 | parameters: 6 | inferPrivatePropertyTypeFromConstructor: true 7 | treatPhpDocTypesAsCertain: true 8 | reportUnmatchedIgnoredErrors: false 9 | ignoreErrors: 10 | - '#Call to static method .* on an unknown class Swoole\\Timer.#' 11 | - '#Instantiated class Swoole\\Http\\Server not found.#' -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | tests/ 14 | 15 | 16 | -------------------------------------------------------------------------------- /publish/apidog.php: -------------------------------------------------------------------------------- 1 | env('APP_ENV') !== 'production', 15 | // swagger 配置的输出文件 16 | // 当你有多个 http server 时, 可以在输出文件的名称中增加 {server} 字面变量 17 | // 比如 /public/swagger/swagger_{server}.json 18 | 'output_file' => BASE_PATH . '/public/swagger/swagger.json', 19 | // 忽略的hook, 非必须 用于忽略符合条件的接口, 将不会输出到上定义的文件中 20 | 'ignore' => function ($controller, $action) { 21 | return false; 22 | }, 23 | // 自定义验证器错误码、错误描述字段 24 | 'error_code' => 400, 25 | 'http_status_code' => 400, 26 | 'field_error_code' => 'code', 27 | 'field_error_message' => 'message', 28 | 'exception_enable' => false, 29 | // swagger 的基础配置 30 | 'swagger' => [ 31 | 'swagger' => '2.0', 32 | 'info' => [ 33 | 'description' => 'hyperf swagger api desc', 34 | 'version' => '1.0.0', 35 | 'title' => 'HYPERF API DOC', 36 | ], 37 | 'host' => 'apidog.cc', 38 | 'schemes' => ['http'], 39 | ], 40 | 'templates' => [ 41 | // // {template} 字面变量 替换 schema 内容 42 | // // 默认 成功 返回 43 | // 'success' => [ 44 | // "code|code" => '0', 45 | // "result" => '{template}', 46 | // "message|message" => 'Success', 47 | // ], 48 | // // 分页 49 | // 'page' => [ 50 | // "code|code" => '0', 51 | // "result" => [ 52 | // 'pageSize' => 10, 53 | // 'total' => 1, 54 | // 'totalPage' => 1, 55 | // 'list' => '{template}' 56 | // ], 57 | // "message|message" => 'Success', 58 | // ], 59 | ], 60 | // golbal 节点 为全局性的 参数配置 61 | // 跟注解相同, 支持 header, path, query, body, formData 62 | // 子项为具体定义 63 | // 模式一: [ key => rule ] 64 | // 模式二: [ [key, rule, defautl, description] ] 65 | 'global' => [ 66 | // 'header' => [ 67 | // "x-token|验签" => "required|cb_token" 68 | // ], 69 | // 'query' => [ 70 | // [ 71 | // 'key' => 'xx|cc', 72 | // 'rule' => 'required', 73 | // 'default' => 'abc', 74 | // 'description' => 'description' 75 | // ] 76 | // ] 77 | ], 78 | ]; 79 | -------------------------------------------------------------------------------- /src/Annotation/ApiController.php: -------------------------------------------------------------------------------- 1 | bindMainProperty('definitions', $value); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Annotation/ApiResponse.php: -------------------------------------------------------------------------------- 1 | description)) { 34 | $this->description = json_encode($this->description, JSON_UNESCAPED_UNICODE); 35 | } 36 | $this->makeSchema(); 37 | } 38 | 39 | public function makeSchema() 40 | { 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Annotation/ApiServer.php: -------------------------------------------------------------------------------- 1 | $val) { 32 | if (property_exists($this, $key)) { 33 | $this->{$key} = $val; 34 | } 35 | } 36 | } 37 | $this->setRequire()->setType(); 38 | } 39 | 40 | public function setRequire() 41 | { 42 | $this->required = strpos(json_encode($this->rules), 'required') !== false; 43 | return $this; 44 | } 45 | 46 | public function setType() 47 | { 48 | $this->type = ''; 49 | 50 | return $this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Annotation/DeleteApi.php: -------------------------------------------------------------------------------- 1 | $val) { 37 | if (property_exists($this, $key)) { 38 | $this->{$key} = $val; 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Annotation/FormData.php: -------------------------------------------------------------------------------- 1 | $val) { 37 | if (property_exists($this, $key)) { 38 | $this->{$key} = $val; 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Annotation/Header.php: -------------------------------------------------------------------------------- 1 | setName()->setDescription()->setRequire()->setType(); 50 | } 51 | 52 | public function setName() 53 | { 54 | $this->name = explode('|', $this->key)[0]; 55 | 56 | return $this; 57 | } 58 | 59 | public function setDescription() 60 | { 61 | $this->description = $this->description ?: explode('|', $this->key)[1] ?? ''; 62 | 63 | return $this; 64 | } 65 | 66 | public function setRequire() 67 | { 68 | $this->required = in_array('required', explode('|', $this->rule)); 69 | 70 | return $this; 71 | } 72 | 73 | public function setType() 74 | { 75 | $type = 'string'; 76 | if (strpos($this->rule, 'int') !== false) { 77 | $type = 'integer'; 78 | } 79 | $this->type = $type; 80 | 81 | return $this; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Annotation/Path.php: -------------------------------------------------------------------------------- 1 | $val) { 37 | if (property_exists($this, $key)) { 38 | $this->{$key} = $val; 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Annotation/PutApi.php: -------------------------------------------------------------------------------- 1 | $val) { 37 | if (property_exists($this, $key)) { 38 | $this->{$key} = $val; 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Annotation/Query.php: -------------------------------------------------------------------------------- 1 | getMethodAnnotations($reflectMethod); 26 | } 27 | 28 | public static function classMetadata($className) 29 | { 30 | return AnnotationCollector::list()[$className]['_c'] ?? []; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/BootAppConfListener.php: -------------------------------------------------------------------------------- 1 | get(LoggerFactory::class)->get('apidog'); 37 | $config = $container->get(ConfigInterface::class); 38 | if (! $config->get('apidog.enable')) { 39 | $logger->debug('apidog not enable'); 40 | return; 41 | } 42 | $output = $config->get('apidog.output_file'); 43 | if (! $output) { 44 | $logger->error('/config/autoload/apidog.php need set output_file'); 45 | return; 46 | } 47 | $servers = $config->get('server.servers'); 48 | if (count($servers) > 1 && ! Str::contains($output, '{server}')) { 49 | $logger->warning('You have multiple serve, but your apidog.output_file not contains {server} var'); 50 | } 51 | foreach ($servers as $server) { 52 | if ($server['type'] != \Hyperf\Server\Server::SERVER_HTTP) { 53 | continue; 54 | } 55 | $router = $container->get(DispatcherFactory::class)->getRouter($server['name']); 56 | $data = $router->getData(); 57 | $swagger = new SwaggerJson($server['name']); 58 | 59 | $ignore = $config->get('apidog.ignore', function ($controller, $action) { 60 | return false; 61 | }); 62 | 63 | array_walk_recursive($data, function ($item) use ($swagger, $ignore) { 64 | if ($item instanceof Handler && ! ($item->callback instanceof \Closure)) { 65 | [$controller, $action] = $this->prepareHandler($item->callback); 66 | (! $ignore($controller, $action)) && $swagger->addPath($controller, $action); 67 | } 68 | }); 69 | 70 | $swagger->save(); 71 | } 72 | } 73 | 74 | protected function prepareHandler($handler): array 75 | { 76 | if (is_string($handler)) { 77 | if (strpos($handler, '@') !== false) { 78 | return explode('@', $handler); 79 | } 80 | return explode('::', $handler); 81 | } 82 | if (is_array($handler) && isset($handler[0], $handler[1])) { 83 | return $handler; 84 | } 85 | throw new \RuntimeException('Handler not exist.'); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | [ 20 | UICommand::class, 21 | ], 22 | 'dependencies' => [ 23 | \Hyperf\HttpServer\Router\DispatcherFactory::class => DispatcherFactory::class, 24 | ], 25 | 'listeners' => [ 26 | BootAppConfListener::class, 27 | ], 28 | 'annotations' => [ 29 | 'scan' => [ 30 | 'paths' => [ 31 | __DIR__, 32 | ], 33 | ], 34 | ], 35 | 'publish' => [ 36 | [ 37 | 'id' => 'config', 38 | 'description' => 'The config for apidog.', 39 | 'source' => __DIR__ . '/../publish/apidog.php', 40 | 'destination' => BASE_PATH . '/config/autoload/apidog.php', 41 | ], 42 | ], 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/DispatcherFactory.php: -------------------------------------------------------------------------------- 1 | getRouter($annotation->server); 29 | 30 | /** @var ApiVersion $version */ 31 | $version = AnnotationCollector::list()[$className]['_c'][ApiVersion::class] ?? null; 32 | foreach ($methodMetadata as $methodName => $values) { 33 | $methodMiddlewares = $middlewares; 34 | // Handle method level middlewares. 35 | if (isset($values)) { 36 | $methodMiddlewares = array_merge($methodMiddlewares, $this->handleMiddleware($values)); 37 | $methodMiddlewares = array_unique($methodMiddlewares); 38 | } 39 | 40 | foreach ($values as $mapping) { 41 | if (! ($mapping instanceof Mapping)) { 42 | continue; 43 | } 44 | if (! isset($mapping->methods)) { 45 | continue; 46 | } 47 | 48 | $tokens = [$version ? $version->version : null, $annotation->prefix, $mapping->path]; 49 | $tokens = array_map(function ($item) { 50 | return ltrim($item, '/'); 51 | }, array_filter($tokens)); 52 | $path = '/' . implode('/', $tokens); 53 | 54 | $router->addRoute($mapping->methods, $path, [$className, $methodName], [ 55 | 'middleware' => $methodMiddlewares, 56 | ]); 57 | } 58 | } 59 | } 60 | 61 | protected function initAnnotationRoute(array $collector): void 62 | { 63 | foreach ($collector as $className => $metadata) { 64 | if (isset($metadata['_c'][ApiController::class])) { 65 | $middlewares = $this->handleMiddleware($metadata['_c']); 66 | $this->handleController($className, $metadata['_c'][ApiController::class], $metadata['_m'] ?? [], $middlewares); 67 | } 68 | } 69 | parent::initAnnotationRoute($collector); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Exception/ApiDogException.php: -------------------------------------------------------------------------------- 1 | response = $response; 45 | $this->request = $request; 46 | $this->validationApi = $validation; 47 | parent::__construct($container, $server->getServerName()); 48 | } 49 | 50 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 51 | { 52 | /** @var Dispatched $dispatched */ 53 | $dispatched = $request->getAttribute(Dispatched::class); 54 | if ($dispatched->status !== Dispatcher::FOUND) { 55 | return $handler->handle($request); 56 | } 57 | 58 | // do not check Closure 59 | if ($dispatched->handler->callback instanceof \Closure) { 60 | return $handler->handle($request); 61 | } 62 | 63 | [$controller, $action] = $this->prepareHandler($dispatched->handler->callback); 64 | 65 | $result = $this->validationApi->validated($controller, $action); 66 | if ($result !== true) { 67 | $config = $this->container->get(ConfigInterface::class); 68 | $exceptionEnable = $config->get('apidog.exception_enable', false); 69 | if ($exceptionEnable) { 70 | $fieldErrorMessage = $config->get('apidog.field_error_message', 'message'); 71 | throw new ApiDogException($result[$fieldErrorMessage]); 72 | } 73 | $httpStatusCode = $config->get('apidog.http_status_code', 400); 74 | return $this->response->json($result)->withStatus($httpStatusCode); 75 | } 76 | 77 | return $handler->handle($request); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Swagger/SwaggerJson.php: -------------------------------------------------------------------------------- 1 | config = $container->get(ConfigInterface::class); 47 | $this->logger = $container->get(LoggerFactory::class)->get('apidog'); 48 | $this->swagger = $this->config->get('apidog.swagger'); 49 | $this->server = $server; 50 | } 51 | 52 | public function addPath($className, $methodName) 53 | { 54 | $ignores = $this->config->get('annotations.scan.ignore_annotations', []); 55 | foreach ($ignores as $ignore) { 56 | AnnotationReader::addGlobalIgnoredName($ignore); 57 | } 58 | $classAnnotation = ApiAnnotation::classMetadata($className); 59 | $controlerAnno = $classAnnotation[ApiController::class] ?? null; 60 | $serverAnno = $classAnnotation[ApiServer::class] ?? null; 61 | $versionAnno = $classAnnotation[ApiVersion::class] ?? null; 62 | $definitionsAnno = $classAnnotation[ApiDefinitions::class] ?? null; 63 | $definitionAnno = $classAnnotation[ApiDefinition::class] ?? null; 64 | $bindServer = $serverAnno ? $serverAnno->name : $this->config->get('server.servers.0.name'); 65 | 66 | $servers = $this->config->get('server.servers'); 67 | $servers_name = array_column($servers, 'name'); 68 | if (! in_array($bindServer, $servers_name)) { 69 | throw new \Exception(sprintf('The bind ApiServer name [%s] not found, defined in %s!', $bindServer, $className)); 70 | } 71 | 72 | if ($bindServer !== $this->server) { 73 | return; 74 | } 75 | 76 | $methodAnnotations = ApiAnnotation::methodMetadata($className, $methodName); 77 | 78 | $headerAnnotation = $classAnnotation[Header::class] ?? null; 79 | $queryAnnotation = $classAnnotation[Query::class] ?? null; 80 | if ($headerAnnotation !== null) { 81 | $methodAnnotations[] = $headerAnnotation; 82 | } 83 | if ($queryAnnotation !== null) { 84 | $methodAnnotations[] = $queryAnnotation; 85 | } 86 | 87 | if (! $controlerAnno || ! $methodAnnotations) { 88 | return; 89 | } 90 | $params = []; 91 | $responses = []; 92 | /** @var \Hyperf\Apidog\Annotation\GetApi $mapping */ 93 | $mapping = null; 94 | $consumes = null; 95 | foreach ($methodAnnotations as $option) { 96 | if ($option instanceof Mapping) { 97 | $mapping = $option; 98 | } 99 | if ($option instanceof Param) { 100 | $params[] = $option; 101 | } 102 | if ($option instanceof ApiResponse) { 103 | $responses[] = $option; 104 | } 105 | if ($option instanceof FormData) { 106 | $consumes = 'application/x-www-form-urlencoded'; 107 | } 108 | if ($option instanceof Body) { 109 | $consumes = 'application/json'; 110 | } 111 | } 112 | if ($mapping === null) { 113 | return; 114 | } 115 | $this->makeDefinition($definitionsAnno); 116 | $definitionAnno && $this->makeDefinition([$definitionAnno]); 117 | 118 | $tag = $controlerAnno->tag ?: $className; 119 | $this->swagger['tags'][$tag] = [ 120 | 'name' => $tag, 121 | 'description' => $controlerAnno->description, 122 | ]; 123 | 124 | $path = $mapping->path; 125 | $prefix = $controlerAnno->prefix; 126 | $tokens = [$versionAnno ? $versionAnno->version : null, $prefix, $path]; 127 | $tokens = array_map(function ($item) { 128 | return ltrim($item, '/'); 129 | }, array_filter($tokens)); 130 | $path = '/' . implode('/', $tokens); 131 | 132 | $method = strtolower($mapping->methods[0]); 133 | $this->swagger['paths'][$path][$method] = [ 134 | 'tags' => [$tag], 135 | 'summary' => $mapping->summary ?? '', 136 | 'description' => $mapping->description ?? '', 137 | 'operationId' => implode('', array_map('ucfirst', explode('/', $path))) . $mapping->methods[0], 138 | 'parameters' => $this->makeParameters($params, $path, $method), 139 | 'produces' => [ 140 | 'application/json', 141 | ], 142 | 'responses' => $this->makeResponses($responses, $path, $method), 143 | ]; 144 | if ($consumes !== null) { 145 | $this->swagger['paths'][$path][$method]['consumes'] = [$consumes]; 146 | } 147 | } 148 | 149 | public function getTypeByRule($rule) 150 | { 151 | $default = explode('|', preg_replace('/\[.*\]/', '', $rule)); 152 | 153 | if (array_intersect($default, ['int', 'lt', 'gt', 'ge', 'integer'])) { 154 | return 'integer'; 155 | } 156 | if (array_intersect($default, ['numeric'])) { 157 | return 'number'; 158 | } 159 | if (array_intersect($default, ['array'])) { 160 | return 'array'; 161 | } 162 | if (array_intersect($default, ['object'])) { 163 | return 'object'; 164 | } 165 | if (array_intersect($default, ['file'])) { 166 | return 'file'; 167 | } 168 | return 'string'; 169 | } 170 | 171 | public function paramObj($in, $value) 172 | { 173 | if ($in == 'body') { 174 | return new Body($value); 175 | } 176 | return new class($value) extends Param {}; 177 | } 178 | 179 | public function golbalParams(): array 180 | { 181 | $conf_global = $this->config->get('apidog.global', []); 182 | $global_params = []; 183 | foreach ($conf_global as $in => $items) { 184 | if (isset($items[0])) { 185 | foreach ($items as $item) { 186 | $global_params[] = $this->paramObj($in, $item); 187 | } 188 | } else { 189 | foreach ($items as $name => $rule) { 190 | $value = [ 191 | 'in' => $in, 192 | 'key' => $name, 193 | 'rule' => $rule, 194 | ]; 195 | $global_params[] = $this->paramObj($in, $value); 196 | } 197 | } 198 | } 199 | return $global_params; 200 | } 201 | 202 | public function makeParameters($params, $path, $method) 203 | { 204 | $this->initModel(); 205 | $method = ucfirst($method); 206 | $path = str_replace(['{', '}'], '', $path); 207 | $parameters = []; 208 | $params = array_merge($params, $this->golbalParams()); 209 | /** @var \Hyperf\Apidog\Annotation\Query $item */ 210 | foreach ($params as $item) { 211 | if ($item->rule !== null && in_array('array', explode('|', $item->rule))) { 212 | $item->name .= '[]'; 213 | } 214 | $name = $item->name; 215 | if (strpos($item->name, '.')) { 216 | $names = explode('.', $name); 217 | $name = array_shift($names); 218 | foreach ($names as $str) { 219 | $name .= "[{$str}]"; 220 | } 221 | } 222 | $parameters[$item->name] = [ 223 | 'in' => $item->in, 224 | 'name' => $name, 225 | 'description' => $item->description, 226 | 'required' => $item->required, 227 | ]; 228 | if ($item instanceof Body) { 229 | $modelName = $method . implode('', array_map('ucfirst', explode('/', $path))); 230 | $this->rules2schema($modelName, $item->rules); 231 | $parameters[$item->name]['schema']['$ref'] = '#/definitions/' . $modelName; 232 | } else { 233 | $type = $this->getTypeByRule($item->rule); 234 | if ($type !== 'array') { 235 | $parameters[$item->name]['type'] = $type; 236 | } 237 | $parameters[$item->name]['default'] = $item->default; 238 | } 239 | } 240 | 241 | return array_values($parameters); 242 | } 243 | 244 | public function makeResponses($responses, $path, $method) 245 | { 246 | $path = str_replace(['{', '}'], '', $path); 247 | $templates = $this->config->get('apidog.templates', []); 248 | 249 | $resp = []; 250 | /** @var ApiResponse $item */ 251 | foreach ($responses as $item) { 252 | $resp[$item->code] = [ 253 | 'description' => $item->description ?? '', 254 | ]; 255 | if ($item->template && Arr::get($templates, $item->template)) { 256 | $json = json_encode($templates[$item->template]); 257 | if (! $item->schema) { 258 | $item->schema = []; 259 | } 260 | $template = str_replace('"{template}"', json_encode($item->schema), $json); 261 | $item->schema = json_decode($template, true); 262 | } 263 | if ($item->schema) { 264 | if (isset($item->schema['$ref'])) { 265 | $resp[$item->code]['schema']['$ref'] = '#/definitions/' . $item->schema['$ref']; 266 | continue; 267 | } 268 | 269 | // 处理直接返回列表的情况 List List 270 | if (isset($item->schema[0]) && ! is_array($item->schema[0])) { 271 | $resp[$item->code]['schema']['type'] = 'array'; 272 | if (is_int($item->schema[0])) { 273 | $resp[$item->code]['schema']['items'] = [ 274 | 'type' => 'integer', 275 | ]; 276 | } elseif (is_string($item->schema[0])) { 277 | $resp[$item->code]['schema']['items'] = [ 278 | 'type' => 'string', 279 | ]; 280 | } 281 | continue; 282 | } 283 | 284 | $modelName = implode('', array_map('ucfirst', explode('/', $path))) . ucfirst($method) . 'Response' . $item->code; 285 | $ret = $this->responseSchemaToDefinition($item->schema, $modelName); 286 | if ($ret) { 287 | // 处理List 288 | if (isset($item->schema[0]) && is_array($item->schema[0])) { 289 | $resp[$item->code]['schema']['type'] = 'array'; 290 | $resp[$item->code]['schema']['items']['$ref'] = '#/definitions/' . $modelName; 291 | } else { 292 | $resp[$item->code]['schema']['$ref'] = '#/definitions/' . $modelName; 293 | } 294 | } 295 | } 296 | } 297 | 298 | return $resp; 299 | } 300 | 301 | public function makeDefinition($definitions) 302 | { 303 | if (! $definitions) { 304 | return; 305 | } 306 | if ($definitions instanceof ApiDefinitions) { 307 | $definitions = $definitions->definitions; 308 | } 309 | foreach ($definitions as $definition) { 310 | /** @var ApiDefinition $definition */ 311 | $defName = $definition->name; 312 | $defProps = $definition->properties; 313 | 314 | $formattedProps = []; 315 | 316 | foreach ($defProps as $propKey => $prop) { 317 | $propKeyArr = explode('|', $propKey); 318 | $propName = $propKeyArr[0]; 319 | $propVal = []; 320 | isset($propKeyArr[1]) && $propVal['description'] = $propKeyArr[1]; 321 | if (is_array($prop)) { 322 | if (isset($prop['description']) && is_string($prop['description'])) { 323 | $propVal['description'] = $prop['description']; 324 | } 325 | 326 | if (isset($prop['type']) && is_string($prop['type'])) { 327 | $propVal['type'] = $prop['type']; 328 | } 329 | 330 | if (isset($prop['default'])) { 331 | $propVal['default'] = $prop['default']; 332 | $type = gettype($propVal['default']); 333 | if (in_array($type, ['double', 'float'])) { 334 | $type = 'number'; 335 | } 336 | ! isset($propVal['type']) && $propVal['type'] = $type; 337 | $propVal['example'] = $propVal['type'] === 'number' ? 'float' : $propVal['type']; 338 | } 339 | if (isset($prop['$ref'])) { 340 | $propVal['$ref'] = '#/definitions/' . $prop['$ref']; 341 | } 342 | } else { 343 | $propVal['default'] = $prop; 344 | $type = gettype($prop); 345 | if (in_array($type, ['double', 'float'])) { 346 | $type = 'number'; 347 | } 348 | $propVal['type'] = $type; 349 | $propVal['example'] = $type === 'number' ? 'float' : $type; 350 | } 351 | $formattedProps[$propName] = $propVal; 352 | } 353 | $this->swagger['definitions'][$defName]['properties'] = $formattedProps; 354 | } 355 | } 356 | 357 | public function responseSchemaToDefinition($schema, $modelName, $level = 0) 358 | { 359 | if (! $schema) { 360 | return false; 361 | } 362 | $definition = []; 363 | 364 | // 处理 Map Map Map 365 | $schemaContent = $schema; 366 | // 处理 List> 367 | if (isset($schema[0]) && is_array($schema[0])) { 368 | $schemaContent = $schema[0]; 369 | } 370 | foreach ($schemaContent as $keyString => $val) { 371 | $property = []; 372 | $property['type'] = gettype($val); 373 | if (in_array($property['type'], ['double', 'float'])) { 374 | $property['type'] = 'number'; 375 | } 376 | $keyArray = explode('|', $keyString); 377 | $key = $keyArray[0]; 378 | $_key = str_replace('_', '', $key); 379 | $property['description'] = $keyArray[1] ?? ''; 380 | if (is_array($val)) { 381 | $definitionName = $modelName . ucfirst($_key); 382 | if ($property['type'] === 'array' && isset($val[0])) { 383 | if (is_array($val[0])) { 384 | $property['type'] = 'array'; 385 | $ret = $this->responseSchemaToDefinition($val[0], $definitionName, 1); 386 | $property['items']['$ref'] = '#/definitions/' . $definitionName; 387 | } else { 388 | $property['type'] = 'array'; 389 | $itemType = gettype($val[0]); 390 | $property['items']['type'] = $itemType; 391 | $property['example'] = [$itemType === 'number' ? 'float' : $itemType]; 392 | } 393 | } else { 394 | // definition引用不能有type 395 | unset($property['type']); 396 | if (count($val) > 0) { 397 | $ret = $this->responseSchemaToDefinition($val, $definitionName, 1); 398 | $property['$ref'] = '#/definitions/' . $definitionName; 399 | } else { 400 | $property['$ref'] = '#/definitions/ModelObject'; 401 | } 402 | } 403 | if (isset($ret)) { 404 | $this->swagger['definitions'][$definitionName] = $ret; 405 | } 406 | } else { 407 | $property['default'] = $val; 408 | $property['example'] = $property['type'] === 'number' ? 'float' : $property['type']; 409 | } 410 | 411 | $definition['properties'][$key] = $property; 412 | } 413 | 414 | if ($level === 0) { 415 | $this->swagger['definitions'][$modelName] = $definition; 416 | } 417 | 418 | return $definition; 419 | } 420 | 421 | public function putFile(string $file, string $content) 422 | { 423 | $pathInfo = pathinfo($file); 424 | if (! empty($pathInfo['dirname'])) { 425 | if (file_exists($pathInfo['dirname']) === false) { 426 | if (mkdir($pathInfo['dirname'], 0644, true) && chmod($pathInfo['dirname'], 0644)) { 427 | return false; 428 | } 429 | } 430 | } 431 | return file_put_contents($file, $content); 432 | } 433 | 434 | public function save() 435 | { 436 | $this->swagger['tags'] = array_values($this->swagger['tags'] ?? []); 437 | $outputFile = $this->config->get('apidog.output_file'); 438 | if (! $outputFile) { 439 | $this->logger->error('/config/autoload/apidog.php need set output_file'); 440 | return; 441 | } 442 | $outputFile = str_replace('{server}', $this->server, $outputFile); 443 | $this->putFile($outputFile, json_encode($this->swagger, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); 444 | $this->logger->debug('Generate swagger.json success!'); 445 | } 446 | 447 | private function initModel() 448 | { 449 | $arraySchema = [ 450 | 'type' => 'array', 451 | 'required' => [], 452 | 'items' => [ 453 | 'type' => 'string', 454 | ], 455 | ]; 456 | $objectSchema = [ 457 | 'type' => 'object', 458 | 'required' => [], 459 | 'items' => [ 460 | 'type' => 'string', 461 | ], 462 | ]; 463 | 464 | $this->swagger['definitions']['ModelArray'] = $arraySchema; 465 | $this->swagger['definitions']['ModelObject'] = $objectSchema; 466 | } 467 | 468 | private function rules2schema($name, $rules) 469 | { 470 | if (! $rules) { 471 | return; 472 | } 473 | $schema = [ 474 | 'type' => 'object', 475 | 'properties' => [], 476 | ]; 477 | foreach ($rules as $field => $rule) { 478 | $type = null; 479 | $property = []; 480 | 481 | $fieldNameLabel = explode('|', $field); 482 | $fieldName = $fieldNameLabel[0]; 483 | if (strpos($fieldName, '.')) { 484 | $fieldNames = explode('.', $fieldName); 485 | $fieldName = array_shift($fieldNames); 486 | $endName = array_pop($fieldNames); 487 | $fieldNames = array_reverse($fieldNames); 488 | $newRules = '{"' . $endName . '|' . $fieldNameLabel[1] . '":"' . $rule . '"}'; 489 | foreach ($fieldNames as $v) { 490 | if ($v === '*') { 491 | $newRules = '[' . $newRules . ']'; 492 | } else { 493 | $newRules = '{"' . $v . '":' . $newRules . '}'; 494 | } 495 | } 496 | $rule = json_decode($newRules, true); 497 | } 498 | if (is_array($rule)) { 499 | $deepModelName = $name . ucfirst($fieldName); 500 | if (Arr::isAssoc($rule)) { 501 | $this->rules2schema($deepModelName, $rule); 502 | $property['$ref'] = '#/definitions/' . $deepModelName; 503 | } else { 504 | $type = 'array'; 505 | $this->rules2schema($deepModelName, $rule[0]); 506 | $property['items']['$ref'] = '#/definitions/' . $deepModelName; 507 | } 508 | } else { 509 | $type = $this->getTypeByRule($rule); 510 | if ($type === 'string') { 511 | in_array('required', explode('|', $rule)) && $schema['required'][] = $fieldName; 512 | } 513 | if ($type == 'array') { 514 | $property['$ref'] = '#/definitions/ModelArray'; 515 | } 516 | if ($type == 'object') { 517 | $property['$ref'] = '#/definitions/ModelObject'; 518 | } 519 | } 520 | if ($type !== null) { 521 | $property['type'] = $type; 522 | if (! in_array($type, ['array', 'object'])) { 523 | $property['example'] = $type; 524 | } 525 | } 526 | $property['description'] = $fieldNameLabel[1] ?? ''; 527 | 528 | $schema['properties'][$fieldName] = $property; 529 | } 530 | $this->swagger['definitions'][$name] = $schema; 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /src/UICommand.php: -------------------------------------------------------------------------------- 1 | get(ConfigInterface::class); 34 | $swagger_file = $config->get('apidog.output_file'); 35 | $servers = $config->get('server.servers'); 36 | $ui = 'default'; 37 | $command = $this; 38 | $host = '0.0.0.0'; 39 | $port = (int) $this->input->getOption('port'); 40 | 41 | if ($config->get('server.type') == \Hyperf\Server\SwowServer::class) { 42 | } else { 43 | $http = new \Swoole\Http\Server($host, $port); 44 | $http->set([ 45 | 'document_root' => $root . '/' . $ui, 46 | 'enable_static_handler' => true, 47 | 'http_index_files' => ['index.html', 'doc.html'], 48 | ]); 49 | 50 | $http->on('start', function ($server) use ($root, $swagger_file, $ui, $command, $host, $port, $servers) { 51 | $command->output->success(sprintf('Apidog Swagger UI is started at http://%s:%s', $host, $port)); 52 | $command->output->text('I will open it in browser after 1 seconds'); 53 | 54 | foreach ($servers as $index => $server) { 55 | $copy_file = str_replace('{server}', $server['name'], $swagger_file); 56 | $copy_json = sprintf('cp %s %s', $copy_file, $root . '/' . $ui); 57 | system($copy_json); 58 | \Swoole\Timer::tick(1000, function () use ($copy_json) { 59 | system($copy_json); 60 | }); 61 | if ($index === 0) { 62 | $index_html = $root . '/' . $ui; 63 | $html = file_get_contents($index_html . '/index_tpl.html'); 64 | $path_info = explode('/', $copy_file); 65 | $html = str_replace('{swagger-json-url}', end($path_info), $html); 66 | file_put_contents($index_html . '/index.html', $html); 67 | } 68 | } 69 | 70 | \Swoole\Timer::after(1000, function () use ($host, $port) { 71 | // TODO win下 72 | system(sprintf('open http://%s:%s', $host, $port)); 73 | }); 74 | }); 75 | 76 | $http->on('request', function ($request, $response) { 77 | $response->header('Content-Type', 'text/plain'); 78 | $response->end("This is apidog server.\n"); 79 | }); 80 | $http->start(); 81 | } 82 | } 83 | 84 | protected function getArguments() 85 | { 86 | $this->addOption('port', 'p', InputOption::VALUE_OPTIONAL, 'Which port you want the SwaggerUi use.', 9939); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Validation/Validation.php: -------------------------------------------------------------------------------- 1 | container = ApplicationContext::getContainer(); 33 | $this->factory = $this->container->get(ValidatorFactory::class); 34 | $this->customValidateRules = $this->container->get(ValidationCustomRule::class); 35 | } 36 | 37 | public function check($rules, $data, $obj = null) 38 | { 39 | foreach ($data as $key => $val) { 40 | if (strpos((string) $key, '.') !== false) { 41 | Arr::set($data, $key, $val); 42 | unset($data[$key]); 43 | } 44 | } 45 | $map = []; 46 | $real_rules = []; 47 | $white_data = []; 48 | 49 | foreach ($rules as $key => $rule) { 50 | $field_extra = explode('|', $key); 51 | $field = $field_extra[0]; 52 | if (! $rule && Arr::get($data, $field)) { 53 | $white_data[$field] = Arr::get($data, $field); 54 | continue; 55 | } 56 | $title = $field_extra[1] ?? $field_extra[0]; 57 | 58 | if (is_array($rule)) { 59 | $has_required = Str::contains('required', json_encode($rule, JSON_UNESCAPED_UNICODE)); 60 | $sub_data = Arr::get($data, $field, []); 61 | if ($has_required && ! $sub_data) { 62 | return [null, [$title . '的子项是必须的']]; 63 | } 64 | 65 | // rule : {"field|字段":"required|***"} 66 | if (Arr::isAssoc($rule)) { 67 | $result = $this->check($rule, $sub_data, $obj); 68 | $result[1] = array_map(function ($item) use ($title) { 69 | return sprintf('%s中的%s', $title, $item); 70 | }, $result[1]); 71 | if ($result[1]) { 72 | return $result; 73 | } 74 | continue; 75 | } // rule : {{"field|字段":"required|***"}} 76 | foreach ($sub_data as $index => $part) { 77 | $result = $this->check($rule[$index] ?? $rule[0], $part, $obj); 78 | $result[1] = array_map(function ($item) use ($title, $index) { 79 | return sprintf('%s中第%s项的%s', $title, $index + 1, $item); 80 | }, $result[1]); 81 | if ($result[1]) { 82 | return $result; 83 | } 84 | } 85 | continue; 86 | } 87 | $_rules = explode('|', $rule); 88 | foreach ($_rules as $index => &$item) { 89 | if ($item == 'json') { 90 | $item = 'array'; 91 | } 92 | if (method_exists($this, $item)) { 93 | $item = $this->makeCustomRule($item, $this); 94 | } elseif (method_exists($this->customValidateRules, $item)) { 95 | $item = $this->makeCustomRule($item, $this->customValidateRules); 96 | } elseif (is_string($item) && Str::startsWith($item, 'cb_')) { 97 | $item = $this->makeObjectCallback(Str::replaceFirst('cb_', '', $item), $obj); 98 | } 99 | unset($item); 100 | } 101 | $real_rules[$field] = $_rules; 102 | $map[$field] = $title; 103 | } 104 | 105 | $validator = $this->factory->make($data, $real_rules, [], $map); 106 | 107 | $verifier = $this->container->get(PresenceVerifierInterface::class); 108 | $validator->setPresenceVerifier($verifier); 109 | 110 | $fails = $validator->fails(); 111 | $errors = []; 112 | if ($fails) { 113 | $errors = $validator->errors()->all(); 114 | 115 | return [ 116 | null, 117 | $errors, 118 | ]; 119 | } 120 | 121 | $filter_data = array_merge($this->parseData($validator->validated()), $white_data); 122 | 123 | $real_data = []; 124 | foreach ($filter_data as $key => $val) { 125 | Arr::set($real_data, $key, $val); 126 | } 127 | 128 | $real_data = array_map_recursive(function ($item) { 129 | return is_string($item) ? trim($item) : $item; 130 | }, $real_data); 131 | 132 | return [ 133 | $fails ? null : $real_data, 134 | $errors, 135 | ]; 136 | } 137 | 138 | public function makeCustomRule($custom_rule, $object) 139 | { 140 | return new class($custom_rule, $object) implements Rule { 141 | public $custom_rule; 142 | 143 | public $validation; 144 | 145 | public $error = '%s '; 146 | 147 | public $attribute; 148 | 149 | public function __construct($custom_rule, $validation) 150 | { 151 | $this->custom_rule = $custom_rule; 152 | $this->validation = $validation; 153 | } 154 | 155 | public function passes($attribute, $value): bool 156 | { 157 | $this->attribute = $attribute; 158 | $rule = $this->custom_rule; 159 | if (strpos($rule, ':') !== false) { 160 | $rule = explode(':', $rule)[0]; 161 | $extra = explode(',', explode(':', $rule)[1]); 162 | $ret = $this->validation->{$rule}($attribute, $value, $extra); 163 | if (is_string($ret)) { 164 | $this->error .= $ret; 165 | 166 | return false; 167 | } 168 | 169 | return true; 170 | } 171 | $ret = $this->validation->{$rule}($attribute, $value); 172 | if (is_string($ret)) { 173 | $this->error .= $ret; 174 | 175 | return false; 176 | } 177 | 178 | return true; 179 | } 180 | 181 | public function message(): array|string 182 | { 183 | return sprintf($this->error, $this->attribute); 184 | } 185 | }; 186 | } 187 | 188 | public function makeObjectCallback($method, $object) 189 | { 190 | return new class($method, $this, $object) implements Rule { 191 | public $custom_rule; 192 | 193 | public $validation; 194 | 195 | public $object; 196 | 197 | public $error = '%s '; 198 | 199 | public $attribute; 200 | 201 | public function __construct($custom_rule, $validation, $object) 202 | { 203 | $this->custom_rule = $custom_rule; 204 | $this->validation = $validation; 205 | $this->object = $object; 206 | } 207 | 208 | public function passes($attribute, $value): bool 209 | { 210 | $this->attribute = $attribute; 211 | $rule = $this->custom_rule; 212 | if (strpos($rule, ':') !== false) { 213 | $rule = explode(':', $rule)[0]; 214 | $extra = explode(',', explode(':', $rule)[1]); 215 | $ret = $this->object->{$rule}($attribute, $value, $extra); 216 | if (is_string($ret)) { 217 | $this->error .= $ret; 218 | 219 | return false; 220 | } 221 | 222 | return true; 223 | } 224 | $ret = $this->object->{$rule}($attribute, $value); 225 | if (is_string($ret)) { 226 | $this->error .= $ret; 227 | 228 | return false; 229 | } 230 | 231 | return true; 232 | } 233 | 234 | public function message(): array|string 235 | { 236 | return sprintf($this->error, $this->attribute); 237 | } 238 | }; 239 | } 240 | 241 | /** 242 | * Parse the data array, converting -> to dots. 243 | */ 244 | public function parseData(array $data): array 245 | { 246 | $newData = []; 247 | 248 | foreach ($data as $key => $value) { 249 | if (is_array($value)) { 250 | $value = $this->parseData($value); 251 | } 252 | 253 | if (Str::contains((string) $key, '->')) { 254 | $newData[str_replace('->', '.', $key)] = $value; 255 | } else { 256 | $newData[$key] = $value; 257 | } 258 | } 259 | 260 | return $newData; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/Validation/ValidationApi.php: -------------------------------------------------------------------------------- 1 | validation = make(Validation::class); 36 | $this->container = ApplicationContext::getContainer(); 37 | $this->config = $this->container->get(ConfigInterface::class); 38 | } 39 | 40 | public function paramObj($in, $value) 41 | { 42 | switch ($in) { 43 | case 'query': 44 | return new Query($value); 45 | case 'formData': 46 | return new FormData($value); 47 | case 'header': 48 | return new Header($value); 49 | case 'body': 50 | return new Body($value); 51 | } 52 | return null; 53 | } 54 | 55 | public function globalParams(): array 56 | { 57 | $conf = $this->config->get('apidog.global', []); 58 | $globalAnno = []; 59 | foreach ($conf as $in => $params) { 60 | $paramsObj = []; 61 | if (isset($params[0])) { 62 | foreach ($params as $param) { 63 | $paramsObj[] = $this->paramObj($in, $param); 64 | } 65 | } else { 66 | if ($in == 'body') { 67 | $globalAnno[] = $this->paramObj($in, [ 68 | 'in' => $in, 69 | 'rules' => $params, 70 | ]); 71 | } else { 72 | foreach ($params as $key => $rule) { 73 | $paramsObj[] = $this->paramObj($in, [ 74 | 'in' => $in, 75 | 'key' => $key, 76 | 'rule' => $rule, 77 | ]); 78 | } 79 | } 80 | } 81 | $globalAnno[] = array_filter($paramsObj); 82 | } 83 | 84 | return $globalAnno; 85 | } 86 | 87 | public function validated($controller, $action) 88 | { 89 | $controllerInstance = $this->container->get($controller); 90 | $request = $this->container->get(ServerRequestInterface::class); 91 | $annotations = array_merge( 92 | ApiAnnotation::methodMetadata($controller, $action), 93 | $this->globalParams() 94 | ); 95 | 96 | $header_rules = []; 97 | $query_rules = []; 98 | $body_rules = []; 99 | $form_data_rules = []; 100 | foreach ($annotations as $annotation) { 101 | if ($annotation instanceof Header) { 102 | $header_rules[$annotation->key] = $annotation->rule; 103 | } 104 | if ($annotation instanceof Query) { 105 | $query_rules[$annotation->key] = $annotation->rule; 106 | } 107 | if ($annotation instanceof Body) { 108 | $body_rules = array_merge($body_rules, $annotation->rules); 109 | } 110 | if ($annotation instanceof FormData) { 111 | $form_data_rules[$annotation->key] = $annotation->rule; 112 | } 113 | } 114 | 115 | if (! array_filter(compact('header_rules', 'query_rules', 'body_rules', 'form_data_rules'))) { 116 | return true; 117 | } 118 | 119 | $config = make(ConfigInterface::class); 120 | $error_code = $config->get('apidog.error_code', -1); 121 | $field_error_code = $config->get('apidog.field_error_code', 'code'); 122 | $field_error_message = $config->get('apidog.field_error_message', 'message'); 123 | 124 | if ($header_rules) { 125 | $headers = $request->getHeaders(); 126 | $headers = array_map(function ($item) { 127 | return $item[0]; 128 | }, $headers); 129 | $real_headers = []; 130 | foreach ($headers as $key => $val) { 131 | $real_headers[implode('-', array_map('ucfirst', explode('-', $key)))] = $val; 132 | } 133 | [ 134 | $data, 135 | $error, 136 | ] = $this->check($header_rules, $real_headers, $controllerInstance); 137 | if ($data === null) { 138 | return [ 139 | $field_error_code => $error_code, 140 | $field_error_message => implode(PHP_EOL, $error), 141 | ]; 142 | } 143 | } 144 | 145 | if ($query_rules) { 146 | [ 147 | $data, 148 | $error, 149 | ] = $this->check($query_rules, $request->getQueryParams(), $controllerInstance); 150 | if ($data === null) { 151 | return [ 152 | $field_error_code => $error_code, 153 | $field_error_message => implode(PHP_EOL, $error), 154 | ]; 155 | } 156 | Context::set(ServerRequestInterface::class, $request->withQueryParams($data)); 157 | } 158 | 159 | if ($body_rules) { 160 | [ 161 | $data, 162 | $error, 163 | ] = $this->check($body_rules, (array) json_decode($request->getBody()->getContents(), true), $controllerInstance); 164 | if ($data === null) { 165 | return [ 166 | $field_error_code => $error_code, 167 | $field_error_message => implode(PHP_EOL, $error), 168 | ]; 169 | } 170 | Context::set(ServerRequestInterface::class, $request->withBody(new SwooleStream(json_encode($data)))); 171 | } 172 | 173 | if ($form_data_rules) { 174 | [ 175 | $data, 176 | $error, 177 | ] = $this->check($form_data_rules, array_merge($request->getUploadedFiles(), $request->getParsedBody()), $controllerInstance); 178 | if ($data === null) { 179 | return [ 180 | $field_error_code => $error_code, 181 | $field_error_message => implode(PHP_EOL, $error), 182 | ]; 183 | } 184 | Context::set(ServerRequestInterface::class, $request->withParsedBody($data)); 185 | } 186 | 187 | isset($data) && Context::set('validator.data', $data); 188 | 189 | return true; 190 | } 191 | 192 | public function check($rules, $data, $controllerInstance) 193 | { 194 | [ 195 | $data, 196 | $error, 197 | ] = $this->validation->check($rules, $data, $controllerInstance); 198 | return [$data, $error]; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Validation/ValidationCustomRule.php: -------------------------------------------------------------------------------- 1 | $val) { 17 | $result[$key] = is_array($val) ? array_map_recursive($func, $val) : call($func, [$val]); 18 | } 19 | 20 | return $result; 21 | } 22 | } 23 | 24 | if (! function_exists('array_sort_by_value_length')) { 25 | function array_sort_by_value_length($arr, $sort_order = SORT_DESC) 26 | { 27 | $keys = array_map('strlen', $arr); 28 | array_multisort($keys, $sort_order, $arr); 29 | return $arr; 30 | } 31 | } 32 | 33 | if (! function_exists('array_sort_by_key_length')) { 34 | function array_sort_by_key_length($arr, $sort_order = SORT_DESC) 35 | { 36 | $keys = array_map('strlen', array_keys($arr)); 37 | array_multisort($keys, $sort_order, $arr); 38 | return $arr; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ui/default/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daodao97/apidog/1891064618e5903b73af21441b69555f4097b488/ui/default/favicon-16x16.png -------------------------------------------------------------------------------- /ui/default/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daodao97/apidog/1891064618e5903b73af21441b69555f4097b488/ui/default/favicon-32x32.png -------------------------------------------------------------------------------- /ui/default/index_tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /ui/default/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Swagger UI: OAuth2 Redirect 4 | 5 | 6 | 7 | 69 | --------------------------------------------------------------------------------