API 文档生成
Viswoole 路由系统内置了 API 文档自动生成能力。启用后,框架会解析路由注解中的元信息(标题、描述、参数、返回值),自动构建结构化的接口文档。
启用配置
在 config/router.php 中开启 API 文档功能:
return [
// ... 其他配置
'api_doc' => [
'enable' => true, // 启用 API 文档生成
'header' => [], // 全局 Header 参数定义
'query' => [], // 全局 Query 参数定义
'body' => [], // 全局 Body 参数定义
'returned' => [], // 全局返回值声明(Returned 实例数组)
],
];配置项说明
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enable | bool | false | 是否启用 API 文档生成 |
header | array | [] | 全局 Header 参数定义 |
query | array | [] | 全局 Query 参数定义 |
body | array | [] | 全局 Body 参数定义 |
returned | array | [] | 全局返回值声明,必须是 Viswoole\Router\ApiDoc\Annotation\Returned 实例数组 |
说明:
header/query/body支持三种配置格式:
FieldStructure实例,如new FieldStructure('authorization', '鉴权令牌', type: Types::String)- 极简格式:
'authorization' => '鉴权令牌'(类型默认string)- 关联数组:
['name' => 'authorization', 'description' => '鉴权令牌', 'type' => 'string']
参数注解
通过自动注入注解声明接口的入参信息,框架会自动解析并纳入文档。这些注解均无构造函数参数,参数名取自 PHP 参数名(不区分大小写)。
自动注入注解
框架提供了便捷的专用注解,用于从不同数据源注入参数:
use Viswoole\HttpServer\AutoInject\{InjectGet, InjectPost, InjectHeader, InjectFile};
#[RouteMapping(paths: ['upload'], method: ['POST'], title: '上传文件')]
public function upload(
#[InjectHeader] string $authorization, // 从 Header 注入
#[InjectGet] string $category, // 从 Query 注入
#[InjectPost] UploadedFile $file, // 从 Body 注入
#[InjectFile] UploadedFile $document, // 从上传文件注入
): array {}注意:这些注解无构造函数参数,参数名必须与 header/query 字段名一致(不区分大小写)。例如
#[InjectHeader] string $authorization会从Authorization请求头中取值。
docblock 类型声明解析
除反射类型外,框架还会解析方法文档注释中 @param 标签的类型声明(PHPStan 风格),用于补充反射无法表达的结构信息(如数组元素类型、关联结构体)。docblock 类型优先于反射类型,解析失败时回退到反射类型:
/**
* 创建订单
*
* @param int $userId 用户ID
* @param array{id: int, name?: string, tags: string[]} $items 商品列表
* @param string[] $coupons 优惠券码列表
*/
#[RouteMapping(paths: ['order/create'], method: ['POST'], title: '创建订单')]
public function create(
#[InjectPost] int $userId,
#[InjectPost] array $items,
#[InjectPost] array $coupons,
): array {}支持的类型语法:
| 语法 | 说明 | 示例 | | ---------- | ------------------------------------------------- | -------------------------------- | -------------------------------- | | 基础类型 | int / string / bool / float 等 | @param string $name | | 数组后缀 | 元素类型加 [],支持多维 | string[]、int[][] | | 联合类型 | | 分隔,括号分组 | int\|string、(int\|string)[] | | 关联结构体 | array{key: type, ...},? 后缀标记可选字段 | array{id: int, name?: string} | | 类 / 枚举 | 完全限定名或全局命名空间短名(不解析 use 别名) | array{\App\Model\User}、Suit |
注意:
- 关联结构体支持嵌套,键名可用引号包裹(含特殊字符的键)。
- 类 / 枚举类型须使用完全限定名称或全局命名空间短名,不会解析
use导入别名。- 未声明 docblock 类型的纯数组参数,结构为
Array<mixed>(无法得知元素类型)。
Returned 注解
Returned 注解用于声明接口返回值的结构:
use Viswoole\Router\ApiDoc\Annotation\Returned;
#[RouteMapping(paths: ['user/list'], method: ['GET'], title: '用户列表')]
#[Returned(
title: '成功',
data: [
'code' => 200,
'message' => '成功',
'data' => [],
],
)]
public function list(): array {}Returned 属性说明
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
title | string | (必填) | 返回值标题 |
data | array | string | (必填) | 示例响应数据,自动推导结构 |
statusCode | int | 200 | HTTP 状态码 |
type | string | application/json | 响应内容类型 |
sort | int | 0 | 排序,数值越大越靠前 |
提示:
data支持传入数组或字符串。传入数组时,框架会自动推导字段类型和嵌套结构;键名支持name|描述和name?|描述语法标记可选字段。
元信息提取
除显式声明的参数注解外,框架还会从 #[RouteMapping] 中自动提取以下元信息:
#[RouteMapping(
paths: ['order/create'],
method: ['POST'],
title: '创建订单', // → 文档标题
description: '提交新订单', // → 文档描述
)]
public function create(): array {}自动提取的字段:
| 来源 | 字段 | 说明 |
|---|---|---|
#[RouteMapping]->title | 标题 | 接口显示名称 |
#[RouteMapping]->description | 描述 | 接口详细说明 |
#[RouteMapping]->method | 请求方法 | GET / POST 等 |
#[RouteMapping]->paths | 请求路径 | 路径列表 |
#[RouteMapping]->tags | 标签 | 接口分类标签,通过 tags 参数显式设置 |
完整示例
<?php
namespace App\Controller\Api\V1;
use Viswoole\HttpServer\AutoInject\{InjectGet, InjectPost, InjectHeader};
use Viswoole\Router\Annotation\{
Controller,
RouteMapping,
};
use Viswoole\Router\ApiDoc\Annotation\Returned;
#[Controller(prefix: '/api/v1')]
class UserController
{
#[RouteMapping(
paths: ['user/login'],
method: ['POST'],
title: '用户登录',
description: '通过手机号和验证码登录',
)]
#[Returned(
title: '登录成功,返回 Token',
data: [
'code' => 200,
'data|访问令牌' => [
'token' => 'string',
'expires_in|有效期(秒)' => 3600,
],
],
)]
public function login(
#[InjectPost] string $phone,
#[InjectPost] string $code,
): array
{
return AuthService::login($phone, $code);
}
#[RouteMapping(
paths: ['user/profile'],
method: ['GET'],
title: '个人信息',
description: '获取当前登录用户的详细信息',
)]
#[Returned(
title: '成功返回用户信息',
data: [
'code' => 200,
'data' => [
'id' => 1,
'name' => '张三',
'avatar?' => null,
],
],
)]
public function profile(
#[InjectHeader] string $authorization,
): array
{
return AuthService::user($authorization);
}
}通过 Router::getApiList() 获取的文档结构大致如下(分组/路由树形结构):
{
"count": 2,
"routes": [
{
"type": "group",
"id": "控制器分组ID",
"parentId": null,
"citeLink": "分组引用链路",
"title": "用户控制器",
"description": "",
"count": 2,
"children": [
{
"type": "route",
"id": "路由ID",
"parentId": "分组ID",
"citeLink": "分组ID.路由ID",
"title": "用户登录",
"description": "通过手机号和验证码登录",
"paths": ["/api/v1/user/login"],
"methods": ["POST"],
"domains": ["*"],
"suffix": ["*"],
"tags": [],
"createdAt": "",
"updatedAt": "",
"author": "",
"meta": [],
"status": {
"value": "development",
"label": "开发中",
"color": "#17a2b8"
}
}
]
}
]
}提示:
hidden为true的路由/分组会被文档生成跳过,不进入上述输出。如需查看某条路由的请求参数与返回值详情,可调用Router::getApiDetail(string $citeLink),其返回['params' => ['body' => ..., 'header' => ..., 'query' => ...], 'returned' => [...]]。
全局参数排除(IgnoreGlobal)
配置了全局参数(header / query / body / returned)后,个别接口可能需要排除其中的部分字段。此时可在控制器类或方法上使用 #[IgnoreGlobal] 注解:
use Viswoole\Router\ApiDoc\Annotation\IgnoreGlobal;
// 排除全部全局配置(header + query + body + returned)
#[IgnoreGlobal]
// 仅排除全局请求头
#[IgnoreGlobal('header')]
// 排除全局请求头与查询参数
#[IgnoreGlobal(['header', 'query'])]
// 任意来源中名为 authorization 的全局字段(如全局鉴权头)
#[IgnoreGlobal(name: 'authorization')]
// 精确排除全局 header 中的 authorization 字段
#[IgnoreGlobal('header', 'authorization')]- 注解可重复标注,多条规则叠加生效;标注到控制器类上时对类内所有路由方法生效。
- 排除仅作用于全局配置;方法上通过
#[InjectHeader]等注解声明的局部参数不受影响,被排除的接口仍可按需声明个别参数。 source可选值为header/query/body/returned,传非法值会抛出异常。
最佳实践
- 保持注解与签名同步:自动注入注解的参数名应与 header/query 字段语义一致
- 必填参数明确标注:通过参数类型(非可空)来声明必填字段
- Returned 描述关键字段:使用
name|描述语法重点标注嵌套结构和可空字段 - title 简洁准确:作为文档目录中的显示名称,应一目了然
