kontirol / apidoc
Generate OpenAPI 3.0 (Swagger) documentation from PHP controller annotations, with optional framework route introspection.
Requires
- php: ^7.4 || ^8.0
- ext-json: *
- symfony/yaml: ^5.4 || ^6.0 || ^7.0
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^9.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-05 15:22:54 UTC
README
从 PHP 控制器的注释生成 OpenAPI 3.0(Swagger) 文档。
不需要手写 YAML,不需要继承任何基类,也不需要跑起来你的应用 —— 它只读源码。
$ vendor/bin/apidoc generate
apidoc 0.1.3
8 endpoint(s) documented, 1 ignored, in 10 ms.
Written:
build/openapi.json (27.4 KB)
build/openapi.yaml (11.2 KB)
生成的 openapi.json 可以直接导入 Swagger UI、Apifox、Postman、Knife4j。
安装
composer require --dev kontirol/apidoc
要求 PHP 7.4 以上(在 8.1 / 8.2 上验证过),唯一的运行时依赖是 symfony/yaml(只有你输出 YAML 时才会用到)。
快速开始
1. 在控制器方法上写注释
<?php namespace App\Api\Controller; class UserController { /** * 分页返回用户列表。 * * @name 用户列表 * @route /api/user/list * @method GET * @tag 用户 * @auth bearer * @param int page=1 页码 * @param int size=10 每页数量 * @param string keyword 昵称关键字 * @response 200 {"code":200,"message":"ok","data":{"total":42,"list":[{"id":1,"nickname":"张三"}]}} */ public function index(): array { return []; } /** * @name 用户详情 * @route /api/user/{id} * @method GET * @tag 用户 * @auth bearer * @param int! id 用户 ID * @response 200 {"code":200,"message":"ok","data":{"id":1,"nickname":"张三"}} * @response 404 {"code":404,"message":"用户不存在"} */ public function detail(int $id): array { return []; } }
规则很简单:
- 只有带注释的方法才会被收集,没有注释的方法直接跳过。
@route里的{id}会自动变成 OpenAPI 的 path 参数,并且强制required: true。- 方法签名里的标量参数(
int $page = 1)会自动补进文档,标量以外的参数(Request $request)会跳过 —— 那是容器注入,不是接口参数。
2. 建一个 apidoc.php
<?php return [ 'controllers' => [ ['path' => __DIR__ . '/app/Api/Controller', 'namespace' => 'App\\Api\\Controller'], ], 'output' => [ 'json' => __DIR__ . '/public/openapi.json', ], 'info' => [ 'title' => '我的项目接口文档', 'version' => '1.0.0', ], 'servers' => ['http://localhost:8000'], ];
也可以写成 JSON 或 YAML,文件名分别是 apidoc.json / apidoc.yaml。
3. 生成
vendor/bin/apidoc generate
在项目根目录下会自动找 apidoc.php、apidoc.json、apidoc.yaml 或 apidoc.yml。
注解速查
| 注解 | 说明 | 例子 |
|---|---|---|
@name |
接口标题(OpenAPI summary) |
@name 用户列表 |
@desc |
详细说明(支持多行,写到下一行即可) | @desc 分页返回用户列表 |
@route |
请求路径,{name} 表示路径变量 |
@route /api/user/{id} |
@method |
HTTP 方法,默认 GET |
@method POST |
@tag / @group |
分组,可写多个 | @tag 用户 |
@auth |
鉴权方式:bearer、apikey、none |
@auth bearer |
@param |
请求参数 | @param int page=1 页码 |
@body |
请求体(JSON 示例或 form/multipart/text) |
@body {"name":"张三"} |
@bodyParam |
表单请求体的字段 | @bodyParam file avatar 头像文件 |
@response |
响应,可写多个状态码 | @response 400 {"code":400} |
@deprecated |
标记为废弃 | @deprecated |
@ignore |
完全排除这个接口 | @ignore |
@param 的写法
@param <类型> <名字>[=<默认值>] <说明>
- 类型后面加
!表示必填:@param int! id 订单 ID - 名字后面跟
=值表示默认值:@param int page=1 页码 - 类型映射:
int/integer→integer,float/number→number,bool→boolean,string[]→array,object/map/json→object,其它一律string - 支持联合类型,取第一个非
null的:@param int|null id 订单 ID
@response 的写法
@response [状态码] {JSON 示例}
不写状态码就是 200。状态码的中文描述(参数错误、未登录或登录已过期……)是自动补的。
@response 200 {"code":200,"data":{"id":1}} @response 400 {"code":400,"message":"参数错误"}
JSON 里的结构会自动反推成 JSON Schema,包括嵌套对象和数组,所以 Swagger UI 里能看到完整的字段树。
@body 的写法
@body {"name":"张三","age":18} // JSON 请求体,自动推断 schema
@body form // 表单
@body form name=张三 age=18 // 表单 + 示例字段
@body multipart // 文件上传,字段用 @bodyParam 声明
URL 从哪来
@route 是最可靠的方式,写什么就是什么。但如果你不想为每个方法都手写一遍,apidoc 也能按 ThinkPHP 的 pathinfo 约定推:
app/api/controller/OrderController.php::myorders()
namespace app\api\controller -> /api
类名 OrderController -> ordercontroller
方法 myorders -> myorders
↓
/api/ordercontroller/myorders
规则:
- 前缀优先取配置里
controllers[].prefix;没配就看命名空间 ——app\{应用}\controller→/{应用},app\controller→ 根 - 类名原样保留(只转小写):ThinkPHP 的
route.controller_suffix出厂值是false,所以OrderController对应/ordercontroller,而不是剥掉后缀的/order - 不做驼峰转下划线:
orderDetail()对应/orderdetail,不是/order_detail;VerifyCode对应/verifycode,不是/verify_code - 命名空间不符合这两个约定时完全不推导,不会给别的框架瞎编 URL
- 推导出来的值伴随一条
route.inferrednotice,方便区分哪些是猜的 - 写了
@route的永远以你写的为准,推导不会覆盖它
这两条规则是从哪知道的
route.source = thinkphp 时,apidoc 会向启动起来的应用问一次:
$app->config->get('route.controller_suffix'); // false,出厂值 $app->config->get('app.url_convert'); // true,URL 转小写
所以通常不用配。只有项目不走 ThinkPHP 的约定时(自己写了路由、用 kebab-case 之类)才需要手动指定:
'url' => [ 'controller_suffix' => true, // auto(默认)| true | false 'case' => 'kebab', // auto(默认)| lower | snake | kebab | keep ],
不想要推导就加 --no-infer,或者在配置里写 'route' => ['infer' => false]。
零注释起步
老项目想先看看「到底有多少接口」,可以先跑一次:
vendor/bin/apidoc generate --all
--all 会把没有 apidoc 标签的方法也全部纳入:
- URL 用上面的规则推导
- HTTP 方法从方法名猜(
saveCode这种驼峰写法也认):save_*/create_*/send_*/notify→POST,remove_*/delete_*/clear_*/cancel_*→DELETE,update_*/edit_*→PUT,其余GET summary用方法名- 参数从方法签名反射出来
- 每一条都带
endpoint.undocumentednotice,明确标明这是猜的
拿到这张接口地图之后,再往注释里补 @name / @param / @response,文档质量自然就上来了。
命令行
apidoc generate [选项] 扫描并写出文档
apidoc validate [选项] 只检查,不写任何文件
apidoc version 打印版本
apidoc help 帮助
| 选项 | 说明 |
|---|---|
-c, --config <文件> |
指定配置文件 |
--no-reflection |
不用 PHP 反射补参数 |
--strict |
把 warning 也当成错误 |
--no-fail-on-empty |
一个接口都没扫到时也不报错 |
--all |
连没有 apidoc 标签的方法也一并文档化(见「零注释起步」) |
--no-infer |
不自动推导缺失的 @route |
-v, --verbose |
打印扫描的目录和 notice 级诊断 |
-q, --quiet |
只打印错误 |
-h, --help |
帮助 |
退出码(方便挂 CI):
| 码 | 含义 |
|---|---|
0 |
成功 |
1 |
跑不起来(配置错、文件不可写……) |
2 |
文档生成了,但有 error 级问题(比如两个接口占用同一个 方法 + 路径) |
validate 会额外检查重复路由、缺失路由、缺失标题,正好适合放进 pre-commit 或 CI:
vendor/bin/apidoc validate --strict
所有诊断码(共 15 个)的含义和修法见 docs/diagnostics.md。
配置项
<?php return [ // 要扫描的控制器目录,可以是字符串、字符串数组,或带命名空间前缀的数组 'controllers' => [ ['path' => 'app/Api/Controller', 'namespace' => 'App\\Api\\Controller'], ], // 输出文件,键是格式(json / yaml),值可以是相对路径 'output' => [ 'json' => 'public/openapi.json', 'yaml' => 'public/openapi.yaml', ], // OpenAPI 的 info 段 'info' => [ 'title' => '接口文档', 'version' => '1.0.0', 'description' => '', ], 'servers' => ['http://localhost:8000'], // 全局鉴权 'security' => [ 'schemes' => [ 'bearerAuth' => ['type' => 'http', 'scheme' => 'bearer', 'bearerFormat' => 'JWT'], ], 'default' => ['bearerAuth' => []], ], // 标签说明 'tags' => ['用户' => '用户中心相关接口'], // 扫描细节 'scan' => [ 'suffix' => 'Controller.php', 'exclude' => ['vendor', 'node_modules', 'tests', 'runtime'], ], 'strict' => false, // 等同 --strict 'fail_on_empty' => true, // 一个接口都没扫到就失败 'reflection' => true, // 用反射补参数 'include_undocumented' => false, // 等同 --all 'route' => [ // annotation(只信 @route)/ thinkphp / auto(默认) 'source' => 'annotation', // 没有写 @route 时,是否按 ThinkPHP 约定从命名空间推导 'infer' => true, // 引导 ThinkPHP 应用时用哪个 autoload(相对配置文件所在目录) 'thinkphp' => [ 'bootstrap' => 'vendor/autoload.php', ], ], // 推导 URL 时用的约定 'url' => [ // 类名里的 Controller 要不要剥掉 // auto(默认)= 问应用;ThinkPHP 出厂值是 false,也就是不剥 'controller_suffix' => 'auto', // 分段大小写:auto(默认)| lower(ThinkPHP 的做法)| snake | kebab | keep 'case' => 'auto', ], ];
相对路径一律相对于配置文件所在目录解析。
它是怎么工作的
ControllerScanner 遍历目录,按后缀 + exclude 规则找候选文件
↓
SourceScanner 用 token_get_all 解析出类、命名空间、public 方法
↓
DocBlockParser 把注释拆成标签(@name 后面的值、多行说明……)
↓
EndpointBuilder 把标签变成 ApiEndpoint(参数、请求体、响应、路径变量)
↓
ReflectionEnricher 可选:用 PHP 反射补齐方法签名里的参数
↓
UrlInferrer 可选:按命名空间约定推导缺失的 @route
↓
RouteMatcher 可选:用框架路由表校验并补全 @route
↓
OpenApiBuilder 渲染成 OpenAPI 3.0 数组(顺手从 JSON 示例推 schema)
↓
DocumentWriter 写 JSON / YAML
整体的设计原则是 能少猜就少猜:
- 反射只做补全,不做反向校验 —— 因为 PHP 框架里大量参数是从
Request::get()拿的,签名里根本没有,按签名校验会满屏误报。 - 推导 URL 时,约定如果问不到框架就退回 ThinkPHP 的出厂值,绝不凭空发明。
- 路由表(如果配了)是运行时真相,如果和
@route冲突,以路由表为准,但会给你一条 warning。 - 所有能修的问题都变成诊断信息(error / warning / notice),而不是直接崩掉。
与 ThinkPHP 配合
apidoc 不依赖任何框架,ThinkPHP 项目用起来和普通 PHP 项目没区别 —— 把控制器目录配好就行。只有两个 ThinkPHP 特有的点:
1. 文件后缀。 ThinkPHP 8 默认 controller_suffix = false,控制器文件叫 Auth.php 而不是 AuthController.php,而 apidoc 默认只扫 *Controller.php:
'scan' => ['suffix' => '.php'],
2. URL 从哪来。 看你的项目是哪种写法:
- pathinfo 自动路由(多应用模式的默认做法,不写
route/*.php)—— ThinkPHP 的路由表里没有业务路由(只有内置的<MISS>),apidoc 按命名空间和类名推导,并按应用自身的route.controller_suffix得到正确路径。不确定推成了什么,加-v看route.inferrednotice,每条都会带上推导结果。 - 注册式路由(写了
Route::get('user/detail', 'User/detail'))—— 可以配route.source = thinkphp,apidoc 会引导你的应用、读出真实路由表,与@route交叉校验,不一致时以真实路由为准并给出 warning;@route没写但路由表里有,还会自动填上。
细节和排查工具见 docs/thinkphp.md。
开发
composer install composer test # phpunit composer analyse # phpstan level max composer check # 两个都跑
想快速看效果,仓库里带了完整的例子:
php bin/apidoc generate -c examples/apidoc.php
# 产物在 build/openapi.json 和 build/openapi.yaml
许可证
MIT