easyswoole / http-annotation
php stander lib
Requires
- php: >=8.1
- ext-dom: *
- ext-json: *
- ext-libxml: *
- ext-mbstring: *
- ext-simplexml: *
- easyswoole/http: 3.x
- easyswoole/parsedown: ^1.0
- psr/http-message: ^1.0
Requires (Dev)
- easyswoole/swoole-ide-helper: ^1.0
- phpunit/phpunit: ^13.4
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- 4.x-dev
- 4.2.3
- 4.2.2
- 4.2.1
- 4.1.9
- 4.1.8
- 4.1.7
- 4.1.6
- 4.1.5
- 4.1.4
- 4.1.3
- 4.1.2
- 4.1.1
- 4.0.11
- 4.0.10
- 4.0.9
- 4.0.8
- 4.0.7
- 4.0.6
- 4.0.5
- 4.0.4
- 4.0.3
- 4.0.2
- 4.0.1
- 3.x-dev
- 3.3.4
- 3.3.3
- 3.3.2
- 3.3.1
- 3.2.22
- 3.2.21
- 3.2.20
- 3.2.19
- 3.2.18
- 3.2.17
- 3.2.16
- 3.2.15
- 3.2.14
- 3.2.13
- 3.2.12
- 3.2.11
- 3.2.10
- 3.2.9
- 3.2.8
- 3.2.7
- 3.2.6
- 3.2.5
- 3.2.3
- 3.2.2
- 3.2.1
- 3.1.10
- 3.1.9
- 3.1.8
- 3.1.6
- 3.1.5
- 3.1.4
- 3.1.3
- 3.1.2
- 3.1.1
- 3.1.0
- 3.0.9
- 3.0.7
- 3.0.6
- 3.0.5
- 3.0.4
- 3.0.3
- 3.0.2
- 3.0.1
- 2.x-dev
- 2.2.2
- 2.2.1
- 2.2.0
- 2.1.0
- 2.0.4
- 2.0.3
- 2.0.2
- 2.0.1
- 2.0.0
- 1.5.0
- 1.4.6
- 1.4.5
- 1.4.4
- 1.4.3
- 1.4.2
- 1.4.1
- 1.4.0
- 1.3.1
- 1.3.0
- 1.2.6
- 1.2.5
- 1.2.4
- 1.2.3
- 1.2.2
- 1.2.1
- 1.2.0
- 1.1.2
- 1.1.1
- 1.1.0
- 1.0.15
- 1.0.14
- 1.0.13
- 1.0.12
- 1.0.11
- 1.0.10
- 1.0.9
- 1.0.8
- 1.0.7
- 1.0.6
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.1
- 1.0.0
- dev-master
This package is auto-updated.
Last update: 2026-10-23 02:46:46 UTC
README
EasySwoole HTTP 控制器的 PHP 原生属性组件。通过 #[Api]、Param 等定义请求方法、参数来源、类型转换、校验规则及接口说明,并生成支持在线试运行的独立 HTML 文档。
安装与环境
composer require easyswoole/http-annotation
需要 PHP 8.1 或以上,以及 JSON、mbstring、DOM、SimpleXML、libxml 扩展。HTTP 处理依赖 easyswoole/http 3.x;运行下面的 Swoole 服务示例还需要实际安装 Swoole 扩展,IDE helper 不提供运行能力。
定义控制器
控制器继承 AnnotationController。ApiGroup 定义文档分组,公开的非静态方法通过 Api 定义接口。
例如将下面的控制器保存为 App/HttpController/Common/Message.php,并在业务项目的 Composer 中配置 App\ 自动加载。
<?php namespace App\HttpController\Common; use EasySwoole\HttpAnnotation\AnnotationController; use EasySwoole\HttpAnnotation\Attributes\Api; use EasySwoole\HttpAnnotation\Attributes\ApiGroup; use EasySwoole\HttpAnnotation\Attributes\Param; use EasySwoole\HttpAnnotation\Enum\ContentType; use EasySwoole\HttpAnnotation\Enum\HttpMethod; use EasySwoole\HttpAnnotation\Enum\ParamFrom; use EasySwoole\HttpAnnotation\Enum\ParamType; use EasySwoole\HttpAnnotation\Validator\IsFile; use EasySwoole\HttpAnnotation\Validator\NotEmpty; use EasySwoole\HttpAnnotation\Validator\Required; #[ApiGroup(groupName: 'Common.Message', description: '消息接口')] class Message extends AnnotationController { #[Api( allowMethod: HttpMethod::GET, requestParam: [ new Param(name: 'page', from: ParamFrom::GET, type: ParamType::INT, value: 1), ], description: '分页读取消息' )] public function list(int $page) { $this->writeJson(200, ['page' => $page]); } #[Api( allowMethod: HttpMethod::POST, acceptContentType: ContentType::JSON, requestParam: [ new Param( name: 'msgId', from: ParamFrom::JSON, type: ParamType::STRING, validate: [new Required(), new NotEmpty()] ), new Param(name: 'testHeader', from: ParamFrom::HEADER), ], description: '根据消息 ID 读取详情' )] public function detail(string $msgId, string $testHeader) { $this->writeJson(200, ['msgId' => $msgId, 'testHeader' => $testHeader]); } #[Api( allowMethod: HttpMethod::POST, acceptContentType: ContentType::FORM_DATA, requestParam: [ new Param( name: 'userThumb', from: ParamFrom::FILE, type: ParamType::FILE, validate: [new Required(), new IsFile()] ), ] )] public function update(array $data) { // $data['userThumb'] 是上传文件对象,由业务代码处理保存。 $this->writeJson(200, ['received' => isset($data['userThumb'])]); } }
普通方法优先按形参名称绑定最终参数定义(包含公共参数和 action 参数)。只有一个 array 或 ?array 形参且不存在同名最终定义时,组件才将接口的 requestParam 打包成数组传入。此收集模式只收集接口参数;公共参数可在 onRequest 中接收。
例如定义 new Param('ids', type: null) 时,batch(array $ids) 接收该字段自身的数组值,如 [1, 2];而 batch(array $data) 在没有 data 参数定义时接收 ['ids' => [1, 2]]。判断依据是是否存在定义,而不是本次请求是否传入;同名字段缺失时仍按定义的默认值绑定。已有收集形参与字段同名的接口需要调整形参名,以免改变接收结构。
在控制器命名空间下注册路由
路由使用 easyswoole/http 的 AbstractRouter。将 Router.php 放在控制器根目录,例如 App/HttpController/Router.php,命名空间必须与 Dispatcher 的控制器命名空间一致:
<?php namespace App\HttpController; use EasySwoole\Http\AbstractInterface\AbstractRouter; use FastRoute\RouteCollector; class Router extends AbstractRouter { public function initialize(RouteCollector $routeCollector) { // 按原始 URL 路径匹配,使路由定义中的前导斜杠和路径保持一致。 $this->setPathInfoMode(false); $routeCollector->addRoute('GET', '/api/messages', '/Common/Message/list'); $routeCollector->addRoute('POST', '/api/messages/detail', '/Common/Message/detail'); $routeCollector->addRoute('POST', '/api/profile/upload', '/Common/Message/update'); } }
服务启动时配置控制器命名空间:
use EasySwoole\Http\Dispatcher; $dispatcher = new Dispatcher(); $dispatcher->setNamespacePrefix('App\\HttpController');
Dispatcher 在首次分发请求时自动查找 App\HttpController\Router 并调用 initialize(),不需要手动实例化 Router。业务项目须配置 App\ 的 Composer 自动加载。
addRoute() 的第三个参数是相对于控制器根命名空间的“控制器路径/action”,例如 /Common/Message/list 会调用 App\HttpController\Common\Message::list()。它不是完整 PHP 类名。请求方法须与对应 Api::allowMethod 一致,路由映射不会绕过参数校验或改变参数来源。
默认未命中自定义路由时仍会尝试按控制器路径分发。若只允许已注册的路由,可在 initialize() 中调用 $this->setGlobalMode(true);未命中的请求将不再回退到控制器路径分发。
当前 Api::registerRouter 尚未实现自动注册;需要如上显式注册。文档生成器也不会读取自定义 Router 的映射,生产文档中的请求地址需与实际路由保持一致。
pathInfo 模式与原始路径
setPathInfoMode(true) 为默认值,匹配前会通过 UrlParser::pathInfo() 处理路径:去掉文件扩展名,并将末尾名称为 index 的文件折叠到父路径。setPathInfoMode(false) 则使用 URI 的原始路径匹配,保留扩展名。两种模式都不包含查询字符串。
| 请求 URL | 开启 pathInfo 时用于匹配的路径 | 关闭 pathInfo 时用于匹配的路径 |
|---|---|---|
/res/example.txt |
/res/example |
/res/example.txt |
/res/example.html?version=1 |
/res/example |
/res/example.html |
/res/index.html |
/res |
/res/index.html |
/Common/Message/list |
/Common/Message/list |
/Common/Message/list |
因此,要区分 .txt 和 .html,需要关闭 pathInfo。此开关只决定路由匹配使用的路径;后续控制器路径分发仍会进行 pathInfo 处理。
正则匹配文件路径并在回调中响应
FastRoute 使用 {参数名:正则} 定义路径参数,不需要加 ^、$ 或正则分隔符。正则内部需要分组时使用非捕获组 (?:...)。下面的路由匹配 /res/example.txt、/res/example.html,文件名允许字母、数字、下划线和连字符:
namespace App\HttpController; use EasySwoole\Http\AbstractInterface\AbstractRouter; use EasySwoole\Http\Request; use EasySwoole\Http\Response; use FastRoute\RouteCollector; class Router extends AbstractRouter { public function initialize(RouteCollector $routeCollector) { $this->setPathInfoMode(false); // 将匹配的 name、ext 注入 GET 参数;同名路由参数优先于原查询参数。 $this->parseParams(self::PARSE_PARAMS_IN_GET); $routeCollector->addRoute( 'GET', '/res/{name:[a-zA-Z0-9_-]+}.{ext:txt|html}', function (Request $request, Response $response) { $params = $request->getQueryParams(); $name = $params['name']; $ext = $params['ext']; $response->withStatus(200); if ($ext === 'html') { $response->withHeader('Content-Type', 'text/html; charset=utf-8'); $response->write('<p>' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '</p>'); } else { $response->withHeader('Content-Type', 'text/plain; charset=utf-8'); $response->write('资源名称:' . $name); } // 已处理请求,终止 Dispatcher 的后续控制器分发。 return false; } ); } }
回调仅接收 Request、Response,没有第三个路径参数数组。默认路径参数保存在协程上下文;上述示例通过 parseParams() 改为注入 GET,控制器也可使用 ParamFrom::GET 读取。write()、withStatus()、withHeader() 设置响应,服务入口在 dispatch() 返回后仍按启动示例调用 $responsePsr->__response() 输出响应。
默认从 Context 读取路由参数
未调用 parseParams() 修改注入方式时,默认使用 AbstractRouter::PARSE_PARAMS_IN_CONTEXT。匹配成功后,所有路径参数以一个数组存入当前协程 Context,键为 AbstractRouter::PARSE_PARAMS_CONTEXT_KEY,不会自动加入 GET 或 POST 参数。
例如沿用 /res/{name:[a-zA-Z0-9_-]+}.{ext:txt|html},去掉上例的 $this->parseParams(self::PARSE_PARAMS_IN_GET),在路由回调或后续控制器 action 中读取:
use EasySwoole\Component\Context\ContextManager; use EasySwoole\Http\AbstractInterface\AbstractRouter; $routeParams = ContextManager::getInstance()->get(AbstractRouter::PARSE_PARAMS_CONTEXT_KEY) ?? []; $name = $routeParams['name'] ?? null; $ext = $routeParams['ext'] ?? null; // 请求 /res/example.html 时: // $routeParams 为 ['name' => 'example', 'ext' => 'html']。
读取应在当前请求处理的协程内完成。未匹配路由或没有该 Context 数据时,上述写法返回空数组。ParamFrom::CONTEXT 按参数名称读取 Context 的顶层键,不会自动从 PARSE_PARAMS_CONTEXT_KEY 数组中取同名字段;需要通过上述方式读取,或改用 parseParams(AbstractRouter::PARSE_PARAMS_IN_GET) 将路径参数注入 GET。
回调如何拦截或继续分发
addRoute() 的第三个参数可以是闭包、可调用对象或其他合法 callable。Dispatcher 根据回调返回值决定下一步:
| 回调返回值 | 后续行为 |
|---|---|
false |
立即结束分发,适合已响应或鉴权失败的请求 |
控制器路径字符串,例如 '/Common/Message/list' |
转交指定控制器 action,即使开启全局模式也会继续 |
true、null 或其他非字符串、非 false 值 |
非全局模式下继续按原请求路径分发;全局模式下结束分发 |
例如,在 initialize() 中注册一个鉴权回调,失败时响应并拦截,通过时转交控制器:
$routeCollector->addRoute('GET', '/api/messages', function (Request $request, Response $response) { // 示例条件;实际项目应替换为自己的鉴权逻辑。 if ($request->getHeaderLine('Authorization') === '') { $response->withStatus(401); $response->withHeader('Content-Type', 'application/json; charset=utf-8'); $response->write(json_encode(['message' => '请先登录'], JSON_UNESCAPED_UNICODE)); return false; } return '/Common/Message/list'; });
如果回调只做日志记录,并希望继续按原控制器路径分发,可注册原路径 /Common/Message/list,处理完返回 true,同时保持 setGlobalMode(false)。继续分发表示进入控制器处理,不会继续执行其他路由;FastRoute 每次请求只选择一个匹配结果。回调抛出异常时,Dispatcher 调用异常处理并终止后续分发。
请求方法、Content-Type 与参数来源
Api::allowMethod 为单个 HttpMethod 枚举,默认 GET,不接受方法数组。
构造函数检查接口定义的一致性,不合规时抛出 Exception\Annotation。文档扫描或首次解析控制器属性时也会触发检查。
| 请求方法 / Content-Type | 允许的请求体参数来源 |
|---|---|
GET、HEAD:acceptContentType 必须为 null |
不允许 POST、JSON、XML、RAW_POST、FILE |
| FORM_DATA | POST、FILE |
| FORM_URLENCODED | POST |
| JSON | JSON |
| XML | XML |
| RAW | RAW_POST |
GET、HEAD 的 Content-Type 默认是 null;其他方法未指定时默认为 FORM_DATA。GET、HEADER、COOKIE、DI、CONTEXT 不属于请求体来源,可与上表中的内容类型一起定义。
特别注意:
Param::from默认是[ParamFrom::GET]。GET、HEAD 接口必须显式指定合规来源,例如from: ParamFrom::GET;JSON、XML、RAW 接口也应显式指定匹配的来源。from可以是单个枚举或数组。数组不能为空,每一项都必须合规;不能把不合规来源当作“备用来源”。读取时按数组顺序选择第一个命中的来源。requestParam必须由Param对象组成,同一接口内不允许重复参数名称。- 这里校验的是 Api 定义。当前控制器运行时会检查 HTTP 方法,但没有单独核验实际请求的
Content-Type是否与acceptContentType一致。需要严格限制实际请求头时,应在业务层增加检查。 onRequest中的公共Param不经过Api构造函数这项来源校验,公共参数也应根据使用它的接口合理定义。
Param 常用配置
| 属性 | 用途与默认值 |
|---|---|
name |
参数名,用于取值和方法形参匹配 |
from |
参数来源数组,默认 [ParamFrom::GET],兼容单个枚举 |
type |
默认 ParamType::STRING;设置为 null 可保留原值 |
value |
未取到参数时的默认值,默认 null |
validate |
校验器对象数组,默认空数组 |
description |
普通字符串或 Text;参数说明不接受 Markdown |
deprecated |
标记废弃,文档显示“已废弃”,不会禁止调用 |
ignoreAction |
忽略该参数的 action 名称列表 |
ignorePassArgWhenNotSet |
单数组形参模式中,未传入时不加入参数数组 |
完整的验证器参数、规则行为和使用示例见 验证器使用指南。
IsUrl(allowProtocols: ['http', 'https']) 可限制 URL 协议;IsIp(mode: 'IPV4') 或 IsIp(mode: 'IPV6') 可限制 IP 版本。这两个规则的校验配置参数位于首位,errorMsg 位于最后;默认分别不额外限制协议、允许 IPv4/IPv6。
支持 STRING、INT、DOUBLE、REAL、FLOAT、BOOLEAN、FILE、NULL_WHILE_EMPTY 类型。转换发生在校验前,转换本身不是合法性校验,必要时仍需配置校验器。
类型转换采用“合法则转换,不合法保留原值”的方式,不因不符合转换规则而抛出异常,也不会改变 hasSet()。例如 INT 的 'abc' 保持原样,再由 Integer() 校验失败;不能仅靠声明 type 保证最终值类型,否则独立的强类型 action 形参仍可能触发类型错误。
| 类型 | 可转换输入 | 其他输入 |
|---|---|---|
| INT | 整数或通过 FILTER_VALIDATE_INT 的字符串(含范围检查) |
保留原值,不截断小数;前导零字符串如 '01' 也保留原值 |
| FLOAT、REAL、DOUBLE | 通过 FILTER_VALIDATE_FLOAT 的有限数值或数值字符串 |
保留原值,不将非法文本转为 0,不截取数字前缀 |
| BOOLEAN | true、false、整数 0/1、字符串 '0'/'1'/'true'/'false'(英文忽略大小写) |
保留原值,空字符串、'yes'、浮点数等不转换 |
| STRING | 字符串、整数、有限浮点数、布尔值 | 数组、对象和非有限浮点数保留原值 |
| NULL_WHILE_EMPTY | 按下文约定转换空值 | 非空值保留原值 |
| FILE | 不转换 | 保留原值 |
INT、BOOLEAN、FLOAT 可分别配合 Integer()、IsBool()、IsFloat() 判断合法性;验证器仍按各自既有规则检查转换后的值。此调整不会让自定义规则或跨字段规则改为读取原始值。
未传参数与空值转换
Param::value 始终保存声明的默认值,请求取值及类型转换不会覆盖它。解析结果通过 parsedValue() 获取,是否实际取到参数通过 hasSet() 判断;解析结果在当前实例中缓存,克隆 Param 会清除解析状态并保留默认值,供新请求重新解析。
ignorePassArgWhenNotSet: true 只在 action 使用单个数组形参收集全部接口参数,且该形参没有同名最终参数定义时生效:没有取到参数(hasSet() === false)时,不将该参数加入传给 action 的数组。按名称绑定单个字段时不应用此选项,即使该字段自身是数组。它不跳过参数校验,因此配置 Required 等规则时,未传参数仍可能校验失败。
GET、POST、JSON 字段通过 isset 判断是否存在,未传字段和显式传入 null 均视为未设置;空字符串 ''、整数 0、字符串 '0'、false 则视为已设置,仍会传入。声明了默认 value 也不会改变 hasSet(),未设置时仍会省略该参数。
type: ParamType::NULL_WHILE_EMPTY 只将空字符串、空数组和 null 转换为 null,保留零值和布尔值:
| 原始值 | 转换后的值 |
|---|---|
null、''、[] |
null |
整数 0、浮点 0.0、字符串 '0'、false |
保留原值和类型 |
空格字符串 ' '、其他非空值 |
保留原值和类型 |
转换不会改变是否已设置参数。组合使用这两个选项时,已传入的空字符串会转为 null,但不会从参数数组中省略。
例如,定义一个可选的请求限制周期参数,action 使用数组接收参数:
use EasySwoole\HttpAnnotation\Attributes\Api; use EasySwoole\HttpAnnotation\Attributes\Param; use EasySwoole\HttpAnnotation\Enum\ParamFrom; use EasySwoole\HttpAnnotation\Enum\ParamType; use EasySwoole\HttpAnnotation\Validator\Optional; #[Api(requestParam: [ new Param( name: 'queryLimitPeriod', from: [ParamFrom::GET], validate: [new Optional()], description: '请求限制周期,单位秒', type: ParamType::NULL_WHILE_EMPTY, ignorePassArgWhenNotSet: true, ), ])] public function updateLimit(array $params): void { // 未传参数:$params 为 []。 // 传入 queryLimitPeriod='':$params 为 ['queryLimitPeriod' => null]。 }
下面对比上述配置中 ignorePassArgWhenNotSet 开启和关闭时的结果。默认值为 null;GET、POST、JSON 字段的是否设置判断规则相同,表格假设参数校验通过:
请求中的 queryLimitPeriod |
hasSet() |
解析后的值 | ignorePassArgWhenNotSet: true 的 action 数组 |
ignorePassArgWhenNotSet: false 的 action 数组 |
|---|---|---|---|---|
| 未传入 | false |
null(不做类型转换) |
[],不包含该字段 |
['queryLimitPeriod' => null] |
显式传入 null |
false |
null(不做类型转换) |
[],不包含该字段 |
['queryLimitPeriod' => null] |
空字符串 '' |
true |
null |
['queryLimitPeriod' => null] |
['queryLimitPeriod' => null] |
空数组 [] |
true |
null |
['queryLimitPeriod' => null] |
['queryLimitPeriod' => null] |
false |
true |
false |
['queryLimitPeriod' => false] |
['queryLimitPeriod' => false] |
浮点数 0.0 |
true |
0.0 |
['queryLimitPeriod' => 0.0] |
['queryLimitPeriod' => 0.0] |
整数 0 |
true |
整数 0 |
['queryLimitPeriod' => 0] |
['queryLimitPeriod' => 0] |
字符串 '0' |
true |
字符串 '0' |
['queryLimitPeriod' => '0'] |
['queryLimitPeriod' => '0'] |
空格字符串 ' ' |
true |
字符串 ' ' |
['queryLimitPeriod' => ' '] |
['queryLimitPeriod' => ' '] |
非空字符串 '30' |
true |
字符串 '30' |
['queryLimitPeriod' => '30'] |
['queryLimitPeriod' => '30'] |
ignorePassArgWhenNotSet 判断的是是否取到参数,不是解析结果是否为 null。NULL_WHILE_EMPTY 只转换空值,不将非空数字字符串转换为整数。
#[Api(requestParam: [
new Param(
name: 'remark',
from: [ParamFrom::GET],
type: ParamType::NULL_WHILE_EMPTY,
ignorePassArgWhenNotSet: true
),
])]
public function update(array $params): void
{
// 未传 remark 或传入 null:$params 中没有 remark。
// 传入 remark='':$params 为 ['remark' => null]。
// 传入 remark='0':$params 为 ['remark' => '0']。
}
- 上传文件同时配置
from: ParamFrom::FILE和type: ParamType::FILE,避免默认字符串转换影响文件对象。 - 未从声明来源取到参数时,不做类型转换,
value默认值原样保留;只有实际取到的值才按type转换。文档默认值列展示声明时的原值,保留0、false、空字符串,null显示为-。 Required检查是否设置参数,NotEmpty检查值是否为空,两者含义不同。Header 参数还应根据需要使用NotEmpty,缺失请求头不会标记为已设置。RequiredIf、RequiredWith、RequiredWithout支持条件必填;同一参数的Optional*(包括IgnoreValidatorWhenEmpty)与Required*、NotEmpty互斥,同时定义会直接抛出配置异常。- 想用
Optional保留“未传入且为 null”的语义时,应显式设置type: null,避免默认 STRING 将null转成空字符串。 - BOOLEAN 仅转换明确的布尔表示:字符串
'false'转为false,'true'转为true;不识别的输入保留原值,建议搭配IsBool()。 - JSON 参数按字段名从请求体解码结果取值;XML 参数从根节点的直接子节点取值;RAW_POST 返回整个请求体。
- JSON/XML 解析在 Swoole 协程中运行:
ArgContext按协程 ID 分别缓存两种格式的解码结果,公共参数和 action 参数可共享;每个协程只注册一次defer,结束时清理缓存,不使用通用ContextManager的键。约定一个请求对应一个协程,请求体在首次解析后不再修改;子协程拥有独立缓存。CLI 测试涉及 JSON/XML 解析时也需在协程中执行。
内置校验器位于 src/Validator,包括必填、长度、数值、日期、邮箱、文件和字段比较等。控制器在公共参数和 action 参数校验阶段均传入已解析的 allDefineParams,支持跨字段比较及条件可选规则;action 同名参数优先,ignoreAction 排除的参数不会进入该集合。
空值跳过校验
NULL_WHILE_EMPTY 只负责类型转换。需要“未传或传空值时跳过全部规则,非空值正常校验”时,显式搭配 IgnoreValidatorWhenEmpty:
use EasySwoole\HttpAnnotation\Validator\IgnoreValidatorWhenEmpty; use EasySwoole\HttpAnnotation\Validator\Integer; use EasySwoole\HttpAnnotation\Validator\Min; new Param( name: 'queryLimitPeriod', from: [ParamFrom::GET], type: ParamType::NULL_WHILE_EMPTY, validate: [new IgnoreValidatorWhenEmpty(), new Integer(), new Min(1)], ignorePassArgWhenNotSet: true, );
在默认值为 null、action 使用单数组收集模式的情况下:
| 输入 | 解析结果 | 校验行为 | action 数组 |
|---|---|---|---|
| 未传 | null |
跳过 | 不包含该字段 |
| 空字符串 | null |
跳过 | 包含该字段,值为 null |
0 或 '0' |
保留原值 | 参与校验,Min(1) 不通过 |
不调用 action |
'10' |
'10' |
正常校验,通过 | 包含该字段,值为 '10' |
'abc' |
'abc' |
正常校验,Integer 不通过 |
不调用 action |
IgnoreValidatorWhenEmpty 在参数未传入,或解析结果为 null、空字符串、空数组时跳过全部规则;0、0.0、'0'、false 和空白字符串仍参与校验。该规则与 Required*、NotEmpty 互斥,也适用于 ObjectShape 内部字段。
嵌套请求参数
ObjectShape 定义对象内部字段,Each 对列表的每个元素执行规则;两者可以递归组合。原有单层 Param 用法不变。
use EasySwoole\HttpAnnotation\Attributes\Api; use EasySwoole\HttpAnnotation\Attributes\Param; use EasySwoole\HttpAnnotation\Enum\ContentType; use EasySwoole\HttpAnnotation\Enum\HttpMethod; use EasySwoole\HttpAnnotation\Enum\ParamFrom; use EasySwoole\HttpAnnotation\Enum\ParamType; use EasySwoole\HttpAnnotation\Validator\Each; use EasySwoole\HttpAnnotation\Validator\IsFloat; use EasySwoole\HttpAnnotation\Validator\Min; use EasySwoole\HttpAnnotation\Validator\MultipleOf; use EasySwoole\HttpAnnotation\Validator\NotEmpty; use EasySwoole\HttpAnnotation\Validator\ObjectShape; use EasySwoole\HttpAnnotation\Validator\Optional; #[Api( allowMethod: HttpMethod::POST, acceptContentType: ContentType::JSON, requestParam: [ new Param( name: 'box_list', from: [ParamFrom::JSON], type: null, description: '包裹列表', validate: [ new NotEmpty(), new Each(validate: [ new ObjectShape(params: [ new Param( name: 'box_length', type: ParamType::FLOAT, description: '包裹长,最多三位小数', validate: [new IsFloat(), new Min(0.001), new MultipleOf(0.001)], ), new Param( name: 'dangerous_detail', type: null, validate: [ new Optional(), new ObjectShape(params: [ new Param(name: 'id', validate: [new NotEmpty()]), ]), ], ignorePassArgWhenNotSet: true, ), ]), ]), ], ), new Param( name: 'shipper_address', from: [ParamFrom::JSON], type: null, validate: [ new NotEmpty(), new ObjectShape(params: [ new Param(name: 'shipper_name', validate: [new NotEmpty()]), ]), ], ), ], )] public function create(array $data): void { // 已完成内部字段转换,例如 box_list[0].box_length 为 float。 }
嵌套字段的约定:
ObjectShape读取当前对象的字段,内部Param::from不参与 HTTP 来源读取,也不允许定义ignoreAction。子字段在文档中继承根参数的来源。Each要求连续、从 0 开始的列表;对象字段使用关联数组表示。JSON 以关联数组解码,空对象{}与空列表[]都变为 PHP[],因此空结构无法严格区分。需要非空时搭配NotEmpty。- 每个对象、每个列表元素独立克隆参数和验证器。内部字段沿用已有类型转换、默认值和
Optional规则;对象中未知字段原样保留。 - 未传字段默认写入其默认值(通常为
null);内部字段设置ignorePassArgWhenNotSet: true时省略。实际传入的空字符串经过NULL_WHILE_EMPTY转为null后仍保留。 - 所有兄弟字段先解析再校验。
RequiredIf、EqualWithColumn等跨字段规则仅查找当前对象的兄弟字段,不自动回退到顶层参数。简单列表元素的规则没有对象字段上下文。 - 校验失败仍抛出
ParamValidateFail;getParamName()和消息中包含完整路径,例如box_list[1].dangerous_detail.id。嵌套层级最多 32 层,超限作为定义错误抛出Annotation。 - 文档自动展开成
box_list、box_list[]、box_list[].box_length等参数行,显示类型、默认值、说明、来源和废弃标记;JSON 的“立即尝试”仍填写完整请求体。
尺寸示例使用 MultipleOf(0.001) 判断数值是否为千分之一的整数倍,允许 11、1.234,不执行四舍五入;它不检查原始文本末尾零的数量。Decimal 的既有语义不变。
公共参数与继承
在 onRequest 上使用可重复的 #[Param] 定义公共参数。接口定义的同名参数优先覆盖公共定义。
参数覆盖、校验与传递规则
控制器先合并定义,再统一解析最终参数。action 同名参数会完整覆盖公共参数的来源、类型、默认值和验证规则,不合并两份验证规则。每个最终参数在单次请求中只执行一次校验,避免 Func 等自定义规则重复执行。
| 参数定义情况 | 最终使用的定义 | 校验次数 | 参数传递位置 |
|---|---|---|---|
| 仅公共参数 | 公共定义 | 1 次 | onRequest |
| 仅 action 参数 | action 定义 | 1 次 | action |
| 公共和 action 同名 | action 完整覆盖 | 1 次 | onRequest 和 action |
公共参数通过 ignoreAction 排除,action 无同名参数 |
不参与当前请求 | 0 次 | 都不传 |
公共参数通过 ignoreAction 排除,action 有同名参数 |
action 定义,覆盖公共参数的排除配置 | 1 次 | onRequest 和 action |
校验顺序为:先按公共参数的声明及继承合并顺序校验,其中同名项使用 action 覆盖后的定义;再按 action 声明顺序校验 action 独有参数。全部参数已提前解析,跨字段规则读取同一份最终参数集合。任一规则失败即停止,不调用 onRequest 和 action。
表中的“传递位置”表示参数所属的接收位置;action 使用单个 array 接收时,未设置且开启 ignorePassArgWhenNotSet 的字段仍会从 action 数组中省略。公共参数单独传给 onRequest,不会因为存在于最终参数集合中就自动加入 action 数组。
use EasySwoole\HttpAnnotation\AnnotationController; use EasySwoole\HttpAnnotation\Attributes\Param; use EasySwoole\HttpAnnotation\Enum\ParamFrom; use EasySwoole\HttpAnnotation\Validator\NotEmpty; abstract class BaseController extends AnnotationController { #[Param(name: 'token', from: ParamFrom::HEADER, validate: [new NotEmpty()])] public function onRequest(?string $action, ?array $data = null): ?bool { // 此处可读取 $data['token'],执行实际鉴权逻辑。 return true; } }
覆盖父类 onRequest 后,使用 #[ExtendParam] 合并父类的公共参数;使用 #[ExtendParam(parentParamsName: ['token'])] 只继承指定参数,子类同名定义优先。ExtendParam 仅对 onRequest 有效。
ignoreAction 可用于公共参数豁免,例如开放接口不要求 token。不要在单数组传参模式中对接口自身的参数使用 ignoreAction:当前数组收集分支仍会访问这些被移除的参数。
同一个 onRequest 内不能重复声明同名 #[Param],否则定义检查会抛出异常,指出参数名、控制器和方法。使用 ExtendParam 时子类覆盖父类同名参数,以及 action 覆盖公共同名参数,仍然允许。
前置回调与属性注入
#[PreCall([Hooks::class, 'before'])] 可用于控制器类或接口方法,回调返回 false 时中止后续执行。
- 类级回调接收
($actionName, $request, $response),在参数解析前执行。 - 方法级回调接收
($request, $response),在参数解析、校验后执行。 #[Di(key: 'service')]和#[Context(key: 'user')]可给控制器公开或受保护属性注入值,同一属性不能同时定义这两种属性。注入发生在方法级前置回调之后。
参数解析和校验在调用父控制器 __hook() 之前执行,异常不进入父方法内部的 onException() 捕获块。接入项目时应核对上层分发器的异常处理,按业务要求返回错误响应。
接口说明与示例
请求示例、成功响应示例、失败响应示例中的合法 JSON/XML 会在生成文档时自动缩进展示,不修改示例对象原始内容。JSON 保留大整数和数字精度;普通文本、无效格式,以及混合文本或显式保留空白的 XML 保持原样。
ApiGroup::description 和 Api::description 支持普通字符串、Text、Markdown。文档全局说明使用 Config::setDescription(),传入 Text 或 Markdown 对象。
use EasySwoole\HttpAnnotation\Bean\Description\Markdown; use EasySwoole\HttpAnnotation\Bean\Description\Text; $markdown = new Markdown(__DIR__ . '/docs/message.md'); $text = new Text('消息接口说明'); $textFile = new Text(__DIR__ . '/docs/message.txt', isFile: true);
Markdown 的构造参数是 Markdown 文件路径,不是 Markdown 内容字符串。生成文档时使用 easyswoole/parsedown 转成 HTML;Text 按纯文本转义展示。说明文件必须存在,建议用 __DIR__ 拼接绝对路径。Markdown 渲染可能保留内嵌 HTML,说明文件应来自可信内容。
Api::requestExamples、responseExamples 接收实现 ExampleInterface 的示例对象数组。错误类型会在定义检查时抛出异常,指出属性、元素索引和实际类型:
use EasySwoole\HttpAnnotation\Bean\Example\Array2Json; use EasySwoole\HttpAnnotation\Bean\Example\ArrayForm; use EasySwoole\HttpAnnotation\Bean\Example\Raw; $requestExamples = [ new Array2Json(['msgId' => '123']), new ArrayForm(['page' => 1]), new Raw('<request><msgId>123</msgId></request>'), ]; $responseExamples = [ new Array2Json(['code' => 200, 'data' => []]), new Array2Json(['code' => 400, 'message' => '参数错误'], isSuccessResponse: false), ];
Raw 也支持 isFile: true。响应示例通过 isSuccessResponse 分到成功、失败区域,各类示例分别从 1 编号。Api::deprecated 只控制废弃标识,不禁止接口执行。
响应参数与完整响应示例
responseParam 描述各字段,responseExamples 展示完整响应,两者分别配置。例如登录方法:
use EasySwoole\HttpAnnotation\Attributes\Api; use EasySwoole\HttpAnnotation\Bean\Example\Array2Json; use EasySwoole\HttpAnnotation\Bean\Example\ResParam; use EasySwoole\HttpAnnotation\Bean\Example\ResParamType; #[Api( responseParam: [ new ResParam('code', 200, ResParamType::int, '响应码,200 表示成功'), new ResParam('result', null, ResParamType::object, '返回数据'), new ResParam('result.access_token', 'token-example', ResParamType::string, '授权 Token'), new ResParam('result.user_info', null, ResParamType::object, '用户信息'), new ResParam('result.user_info.id', 19, ResParamType::int, '用户 ID'), new ResParam('msg', 'Success', ResParamType::string, '响应消息'), ], responseExamples: [ new Array2Json([ 'code' => 200, 'result' => [ 'access_token' => 'token-example', 'user_info' => ['id' => 19], ], 'msg' => 'Success', ]), new Array2Json([ 'code' => 401, 'result' => null, 'msg' => '账号或密码错误', ], isSuccessResponse: false), ] )] public function login(): void { // 实际登录逻辑与响应由业务实现。 }
ResParam 依次接收 name、exampleValue、paramType、description,类型支持 ResParamType::object、array、string、int、bool、float;说明支持字符串或 Text。responseParam 只接受 ResParam,不再接受请求参数对象 Param;空名称、纯空白名称和重复字段名会在构造 Api 时抛出配置异常,扫描控制器时还会附加类名及方法名。
嵌套字段使用点分名称,按声明顺序逐行展示,不会据此自动构造响应 JSON。数组字段可以这样描述,[] 仅为文档中的名称约定:
new ResParam('result.items', [['id' => 1]], ResParamType::array, '数据列表'); new ResParam('result.items[].id', 1, ResParamType::int, '列表元素 ID');
生成的文档在“请求参数”后显示四列响应参数表,再展示请求示例、成功响应示例及失败响应示例。响应参数为空时显示“暂无示例”。这些定义仅用于文档展示,不会校验实际响应,也不会自动设置 HTTP 状态码;示例中的 code 是响应体字段。
生成接口文档
<?php require __DIR__ . '/vendor/autoload.php'; use EasySwoole\HttpAnnotation\Bean\Description\Markdown; use EasySwoole\HttpAnnotation\Document\Document; $document = new Document( __DIR__ . '/App/HttpController', 'App\\HttpController' ); $document->getConfig()->setProjectName('消息服务'); $document->getConfig()->setHost('http://127.0.0.1:9501'); $document->getConfig()->setDescription(new Markdown(__DIR__ . '/docs/introduction.md')); file_put_contents(__DIR__ . '/api.html', $document->scan2html()); // 也可 echo $document->scan2html(),作为 HTTP 响应输出。
scanAllApiGroup()收集控制器属性;scan2ArrayMap()返回文档树;scan2html()返回完整 HTML 字符串,不自动保存文件。- 文档源码按职责维护:
src/Document/doc.tpl是页面骨架,src/Document/assets/保存 CSS 及渲染、搜索、导航、在线调试等 JavaScript 片段。生成时按模板中的资源占位符顺序内联,输出仍是独立 HTML,无需额外部署 assets 或运行前端构建工具。 - 扫描的控制器需能被 Composer 自动加载、继承
AnnotationController且定义ApiGroup。仅带Api的公开非静态方法进入文档;每个文件目前只使用扫描到的第一个类。 - 分组名称不可重复。
Common.Message、Common.Profile生成 Common → Message / Profile → action 的侧边栏。没有接口且没有有效后代的节点不展示。 - 控制器命名空间参数必须与实际目录对应。文档路径由控制器类名和方法名生成,各路径层级首字母转小写,尾部 Index 控制器与 index action 有简写处理。
Api构造函数当前没有requestPath参数,不要使用requestPath: ...;registerRouter也尚未实现自动注册路由。文档路径应与业务路由配置一致。responseParam使用ResParam定义响应字段,文档在请求参数后展示“响应参数”表(参数名、参数值、参数类型、描述说明)。嵌套字段可用result.user.id这样的点分名称逐行定义,按声明顺序展示。- 修改定义后应重新生成 HTML、刷新浏览器;常驻服务中的属性缓存还需要通过重启相关工作进程刷新。
“立即尝试”使用与限制
点击 action 标题旁的“立即尝试”,填写参数后运行。
- 请求方法固定使用
Api::allowMethod,不可在窗口内切换;地址可编辑,默认由配置 host 和接口路径组合。 - 表单类型显示参数输入框,预填
Param::value。勾选的参数才发送,编辑参数后会自动勾选;FILE 类型显示文件选择器,选中文件后自动勾选。 - JSON、XML、RAW 使用一个多行输入框提交完整请求体,JSON/XML 会检查格式。Header 和 URL 参数仍可单独填写。
- Header 来源的参数作为 HTTP 请求头发送;GET 来源作为 URL 查询参数发送。DI、CONTEXT 不由浏览器填写,Cookie 由浏览器管理。
- 所有 HTTP 状态都显示,包括 4xx、5xx;结果区展示状态码、耗时、响应内容及浏览器允许读取的响应头。JSON 响应会格式化,空响应也有提示。
- 超时可设置为 1–300 秒。网络异常、超时和取消会显示结果;点击关闭或在弹窗内按 Esc 会取消正在进行的请求。
- 打开弹窗时焦点移到关闭按钮,关闭后回到“立即尝试”。焦点在浏览器地址栏、开发者工具或其他窗口时,页面无法接收 Esc。
特别注意浏览器请求的实际限制:
- 建议通过 HTTP 服务访问生成文档。例如在仓库目录执行
php -S 127.0.0.1:8080,再打开http://127.0.0.1:8080/api.html。该服务只用于静态文档预览,业务接口仍需运行自己的服务。 - 请求使用
credentials: 'include'。跨域接口应允许文档的具体 Origin、凭据、请求方法和自定义 Header;需要凭据时不能用Access-Control-Allow-Origin: *。HTTPS 文档请求 HTTP 接口还可能被混合内容策略阻止。 - 浏览器禁止设置的请求头不能由表单强行发送;跨域响应头的可见范围由服务端 CORS 配置决定。连接失败或 CORS 拦截时通常无法获得 HTTP 状态码。
- RAW 试运行默认使用
text/plain。当前没有单独配置 RAW MIME 类型的界面。 - 当前普通表单在有文件时使用 multipart/form-data,无文件时使用 URL 编码表单;即使 Api 声明 FORM_DATA,也不会强制无文件表单使用 multipart。服务端严格限制内容类型时需要注意这一差异。
- HTML 是生成时的静态快照,示例数据、默认值和说明会写入文件,公开前检查是否包含不应公开的信息。
启动 Swoole HTTP 服务
<?php require __DIR__ . '/vendor/autoload.php'; use EasySwoole\Http\Dispatcher; use EasySwoole\Http\Request; use EasySwoole\Http\Response; use Swoole\Http\Server; $dispatcher = new Dispatcher(); $dispatcher->setNamespacePrefix('App\\HttpController'); $http = new Server('127.0.0.1', 9501); $http->set(['worker_num' => 1]); $http->on('request', function ($request, $response) use ($dispatcher) { $requestPsr = new Request($request); $responsePsr = new Response($response); $dispatcher->dispatch($requestPsr, $responsePsr); $responsePsr->__response(); }); $http->start();
控制器路径、命名空间及文档 host 需要与业务项目保持一致。本仓库的可运行文档生成示例见 test.php,控制器示例见 tests/ControllerExample。
验证
开发测试使用 PHPUnit 13.4,需要 PHP 8.4 或以上;库本身的最低 PHP 版本仍为 8.1。安装开发依赖后,默认读取 phpunit.xml.dist,可运行完整测试或指定目录:
php vendor/bin/phpunit php vendor/bin/phpunit tests/Attributes php vendor/bin/phpunit tests/Document node tests/Document/try-runner.test.cjs node tests/Document/document-ui.test.cjs
Node 测试使用内置的 Fetch、File、FormData 等 API,需使用提供这些全局对象的现代 Node.js(建议 20+)。前端交互测试采用 DOM 替身;浏览器焦点、布局和跨域行为仍需在实际浏览器与服务环境中验证。
验证器测试通过 Param(type: null, ...) 保留原始输入类型,专门验证规则行为;默认 ParamType::STRING 会先将输入转换为字符串。PHPUnit 数据提供器使用 #[DataProvider(...)],提供器方法必须为 public static。
参数来源包含 ParamFrom::FILE 时,只允许单一来源 from: [ParamFrom::FILE](兼容 from: ParamFrom::FILE);与其他来源混用或重复定义 FILE 会抛出异常。