├── .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 | 
353 |
354 | ## Swagger展示
355 |
356 | 
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 | 
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