API 文档生成

路由系统内置 API 文档生成能力:开启 router.api_doc.enable 后,框架在路由注册阶段解析处理器签名中的注入注解、PHPDoc 与返回值注解,构建结构化的接口文档(参数结构 + 响应结构),并可编程查询。

启用与配置

php
// config/router.php
'api_doc' => [
  // 是否启用
  'enable'   => true,
  // 全局返回值声明,值为 Viswoole\Router\ApiDoc\Annotation\Returned 实例数组
  'returned' => [],
  // 全局请求头参数,格式见下文
  'header'   => [],
  // 全局查询参数(GET),格式同 header
  'query'    => [],
  // 全局请求体参数(POST),格式同 header
  'body'     => [],
],
配置键类型默认值说明
enableboolfalse是否启用 API 文档生成
returnedarray[]全局返回值声明,必须是 Returned 实例数组
headerarray[]全局请求头参数
queryarray[]全局查询参数
bodyarray[]全局请求体参数

全局参数(header/query/body)支持三种格式,启动时统一归一化为 FieldStructure 实例:

php
use Viswoole\Router\ApiDoc\Structure\{FieldStructure, Types};

'header' => [
  // 格式一:FieldStructure 实例(可精确控制类型)
  new FieldStructure('authorization', '鉴权令牌', type: Types::String),
  // 格式二:极简格式,'参数名' => '描述'(类型默认 string)
  'x-app-id' => '应用 ID',
  // 格式三:关联数组
  ['name' => 'x-sign', 'description' => '签名', 'type' => 'string'],
],

其中数组格式的 type 支持 Types 枚举或类型字符串(string/int/float/bool/array/object/null,其他值按 mixed 处理);FieldStructure 的完整构造参数为 namedescriptionallowNulldefaulttype

请求参数声明

参数来源由 Inject 注解标记

参数出现在文档的哪个分组,由参数上的自动注入注解决定(注解命名空间 Viswoole\HttpServer\AutoInject,无构造参数):

注解参数来源文档分组
#[InjectGet]GET 查询参数query
#[InjectPost]POST 请求体body
#[InjectHeader]请求头header
#[InjectFile]上传文件body(类型标记为 file)
php
use Viswoole\HttpServer\AutoInject\{InjectGet, InjectPost, InjectHeader, InjectFile};
use Viswoole\HttpServer\Message\UploadedFile;
use Viswoole\Router\Annotation\{Controller, RouteMapping};

#[Controller(prefix: 'user')]
class UserController
{
  /**
   * 更新头像
   */
  #[RouteMapping(method: 'POST', title: '更新头像')]
  public function avatar(
    #[InjectHeader] string $authorization,  // header 参数
    #[InjectGet] int $from = 1,             // query 参数(有默认值,可选)
    #[InjectFile] UploadedFile $file,       // file 参数(单文件)
  ): array { return []; }
}

未标注来源注解的处理器参数不会进入文档。文件参数:类型含 array 时视为多文件(UploadedFile[]),否则视为单文件。

参数描述与类型

  • 描述取自方法 PHPDoc 中 @param 标签的描述部分
  • 类型优先取 @param 标签的 PHPStan 风格类型声明(可表达反射无法描述的结构),解析失败时回退到参数反射类型
php
/**
 * 创建订单
 *
 * @param int $userId 用户ID
 * @param array{id: int, name?: string, tags: string[]} $items 商品列表
 */
#[RouteMapping(method: 'POST', title: '创建订单')]
public function create(
  #[InjectPost] int $userId,
  #[InjectPost] array $items,
): array {}

支持的 docblock 类型语法:

语法说明示例
基础类型int / string / bool / float@param string $name
数组后缀元素类型加 [],支持多维string[]int[][]
联合类型| 分隔,可括号分组int|string(int|string)[]
关联结构体array{key: type},键名 ? 后缀标记可选字段,支持嵌套array{id: int, name?: string}
类 / 枚举完全限定名或全局命名空间短名(不解析 use 别名)\App\Enum\Suit

参数是否可空由参数类型是否允许 null(含默认值 null)决定;参数默认值会写入文档。

Returned 注解(返回值)

#[Returned] 声明接口的响应结构,标注在处理器方法上,可重复标注(每种响应一个注解)。命名空间 Viswoole\Router\ApiDoc\Annotation

php
use Viswoole\Router\ApiDoc\Annotation\Returned;

#[Returned('成功', ['id' => '用户ID', 'name?' => '姓名,可选', 'roles' => [['id' => '角色ID']]], 200)]
#[Returned('创建成功', ['code' => '状态码'], 201)]
public function create(): array {}

属性说明

属性类型默认值说明
titlestring必填该响应的标题
dataarray|string必填示例响应数据,自动推导结构
statusCodeint200HTTP 状态码
typestringapplication/json响应内容类型,内置常量 TYPE_JSON / TYPE_XML / TYPE_HTML / TYPE_TEXT / TYPE_STREAM
sortint0排序,数值越大越靠前

data 结构推导规则

data 传入数组时,框架按示例值递归推导字段类型:

  • 键名语法'字段|描述' 携带字段描述,'字段?|描述' 标记可选字段(允许 null
  • 关联数组:视为对象结构,逐字段解析;索引数组:视为数组结构(Array<元素类型>
  • 嵌套数组:递归解析,如 'roles' => [['id' => '角色ID']] 推导为对象数组
  • 枚举值:Backed 枚举取 backing value(int/string),纯枚举取枚举项名(string
  • 对象:解析其公共属性生成结构
  • 字符串:整个 data 为字符串时按 string 类型处理
php
#[Returned('成功', [
  'code' => 200,                        // int
  'message' => '成功',                   // string
  'data|访问令牌' => [                    // 嵌套对象,带描述
    'token' => 'eyJ0eXAi...',           // string
    'expires_in|有效期(秒)' => 3600,    // int,带描述
  ],
  'avatar?' => null,                    // 可选字段,null 类型
])]
public function login(): array {}

全局参数排除(IgnoreGlobal)

配置全局参数后,个别接口可能不需要其中部分字段。#[IgnoreGlobal](命名空间 Viswoole\Router\ApiDoc\Annotation,可重复标注)可排除全局配置的生效范围,支持标注在控制器类(对类内全部路由生效)或方法上,两类规则叠加:

php
use Viswoole\Router\ApiDoc\Annotation\IgnoreGlobal;

#[IgnoreGlobal]                                      // 排除全部全局配置(header+query+body+returned)
#[IgnoreGlobal('header')]                            // 仅排除全局请求头
#[IgnoreGlobal(['header', 'query'])]                 // 排除全局请求头与查询参数
#[IgnoreGlobal(name: 'authorization')]               // 任意来源中名为 authorization 的全局字段
#[IgnoreGlobal('header', 'authorization')]           // 精确排除全局 header 中的 authorization 字段
public function login(): array {}
参数类型默认值说明
sourcestring|array|nullnull排除的来源,有效值 header / query / body / returnednull 表示全部;传非法值会抛出异常
namestring|array|nullnull排除的字段名;null 表示该来源全部字段;returned 来源时按返回声明的标题匹配

INFO

方法上通过 #[InjectHeader] 等注解声明的局部参数不受 #[IgnoreGlobal] 影响,被排除的接口仍可按需声明个别参数。

类型与 TypeScript 的对应

文档输出的类型以 TypeScript(TS)兼容为设计目标,由文档渲染端完成映射:

框架类型TS 类型
int / floatnumber
boolboolean
mixedany
string / array / objectstring / Array<T> / object

SUCCESS

类型越具体文档越有价值:编写 Returned 示例数据与 @param 类型时,优先给出 int/string 等具体类型或 array{id: int} 结构体,避免笼统的 mixedobject

查询 API 文档

文档数据通过 Router 门面编程获取,可用于自建文档站点或调试:

php
use Viswoole\Router\Facade\Router;

// 获取全部接口文档列表(hidden 为 true 的分组及分组内路由不会进入列表)
$list = Router::getApiList();   // ['count' => N, 'routes' => [...]]

// 获取指定接口的参数与返回值详情(citeLink 取自列表项)
$detail = Router::getApiDetail($citeLink);  // ['params' => [...], 'returned' => [...]]

列表项为分组(type: "group")与路由(type: "route")组成的树,路由节点包含 idparentIdciteLinktitledescriptionpathsmethodsdomainssuffixtagsauthorcreatedAtupdatedAtmetasource(源码位置)与 status(含 value/中文 label/展示 color):

json
{
  "type": "route",
  "id": "路由ID",
  "parentId": "分组ID",
  "citeLink": "分组ID.路由ID",
  "title": "创建订单",
  "paths": ["/api/v1/order/create"],
  "methods": ["POST"],
  "tags": [],
  "status": { "value": "development", "label": "开发中", "color": "#17a2b8" }
}

getApiDetail($citeLink) 返回的 paramsbody / header / query 三个来源分组,每项为 FieldStructure 结构(字段名、描述、是否可空、默认值、类型);returned 为按 sort 排序的响应声明列表。

WARNING

确认 router.api_doc.enable 已设为 true——该功能关闭时 Route 实例不会解析参数与返回值注解,getApiList() 始终返回空列表。

下一步