API 文档生成

Viswoole 路由系统内置了 API 文档自动生成能力。启用后,框架会解析路由注解中的元信息(标题、描述、参数、返回值),自动构建结构化的接口文档。

启用配置

config/router.php 中开启 API 文档功能:

php
return [
    // ... 其他配置

    'api_doc' => [
        'enable'   => true,   // 启用 API 文档生成
        'header'   => [],      // 全局 Header 参数定义
        'query'    => [],      // 全局 Query 参数定义
        'body'     => [],      // 全局 Body 参数定义
        'returned' => [],      // 全局返回值声明(Returned 实例数组)
    ],
];

配置项说明

配置项类型默认值说明
enableboolfalse是否启用 API 文档生成
headerarray[]全局 Header 参数定义
queryarray[]全局 Query 参数定义
bodyarray[]全局 Body 参数定义
returnedarray[]全局返回值声明,必须是 Viswoole\Router\ApiDoc\Annotation\Returned 实例数组

说明header / query / body 支持三种配置格式:

  • FieldStructure 实例,如 new FieldStructure('authorization', '鉴权令牌', type: Types::String)
  • 极简格式:'authorization' => '鉴权令牌'(类型默认 string
  • 关联数组:['name' => 'authorization', 'description' => '鉴权令牌', 'type' => 'string']

参数注解

通过自动注入注解声明接口的入参信息,框架会自动解析并纳入文档。这些注解均无构造函数参数,参数名取自 PHP 参数名(不区分大小写)。

自动注入注解

框架提供了便捷的专用注解,用于从不同数据源注入参数:

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 类型优先于反射类型,解析失败时回退到反射类型:

php
/**
 * 创建订单
 *
 * @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 注解用于声明接口返回值的结构:

php
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 属性说明

属性类型默认值说明
titlestring(必填)返回值标题
dataarray | string(必填)示例响应数据,自动推导结构
statusCodeint200HTTP 状态码
typestringapplication/json响应内容类型
sortint0排序,数值越大越靠前

提示data 支持传入数组或字符串。传入数组时,框架会自动推导字段类型和嵌套结构;键名支持 name|描述name?|描述 语法标记可选字段。

元信息提取

除显式声明的参数注解外,框架还会从 #[RouteMapping] 中自动提取以下元信息:

php
#[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
<?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() 获取的文档结构大致如下(分组/路由树形结构):

json
{
  "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"
          }
        }
      ]
    }
  ]
}

提示hiddentrue 的路由/分组会被文档生成跳过,不进入上述输出。如需查看某条路由的请求参数与返回值详情,可调用 Router::getApiDetail(string $citeLink),其返回 ['params' => ['body' => ..., 'header' => ..., 'query' => ...], 'returned' => [...]]

全局参数排除(IgnoreGlobal)

配置了全局参数(header / query / body / returned)后,个别接口可能需要排除其中的部分字段。此时可在控制器类或方法上使用 #[IgnoreGlobal] 注解:

php
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,传非法值会抛出异常。

最佳实践

  1. 保持注解与签名同步:自动注入注解的参数名应与 header/query 字段语义一致
  2. 必填参数明确标注:通过参数类型(非可空)来声明必填字段
  3. Returned 描述关键字段:使用 name|描述 语法重点标注嵌套结构和可空字段
  4. title 简洁准确:作为文档目录中的显示名称,应一目了然