用 Accept Header 实现版本路由最干净,即通过 Accept: application/vnd.myapp.v2+json 传递版本,保持路径统一为 /api/users,再由中间件解析并注入版本上下文,避免硬编码路由和重复解析。
用
Header 实现版本
路由
最干净
PHP API 版本控制不是靠改 URL 路径(比如
),而是优先用 HTTP
头,比如
。这样接口路径不变,语义清晰,也方便客户端做灰度或回滚。
常见错误是把版本硬编码进路由——结果一加
就得复制整套控制器、中间件、测试用例,维护成本翻倍。
用 Laravel 时,在路由定义里不写
,而是统一走
,再在中间件里解析
头
用原生 PHP 或 Slim 时,别在
里写死,改用
+ 请求头判断
注意
值必须带
前缀(如
),否则和标准 MIME 类型冲突,某些网关或 CDN 会直接拦截
+ 中间件提取版本号
Laravel 用户最容易踩的坑是:在中间件里用
手动 parse,但没做缓存或 fallback,导致每次请求都正则匹配,性能掉得明显。
推荐做法是用路由分组 + 自定义中间件提前注入版本上下文:
立即学习
“
PHP免费学习笔记(深入)
”;
PHP 8.5.5
PHP 8.5.5 是 PHP 8.5 分支的维护更新版本。该版本延续了“小步快跑”的迭代逻辑,通过深度错误修复、底层性能微调以及安全加固,旨在为开发者提供一个更健壮、更高效的运行环境。该版本严格遵守语义化版本规范,不包含破坏性变更。
下载
在
里定义统一入口:
中间件里只做一次解析,把版本存到
,后续控制器直接读取
别在每个控制器方法里重复调用
或
,容易漏掉大小写或空格处理
如果用了 API 文档工具(如 Swagger),记得在
里声明
支持的值,否则生成的 SDK 默认不带这个头
URL 路径带版本仅用于降级兼容
路径里放
不是错,但只该用在「老客户端无法发 Header」这种真实约束下,比如嵌入式设备、旧版 iOS App、或第三方系统对接。
这时候不能让
和
指向两套完全独立的代码,否则很快失控。
所有版本路由最终都 dispatch 到同一个控制器,只是传入不同
参数
用策略模式组织业务逻辑:比如
和
,避免 if-else 堆砌
别让
的响应结构和
差太多——字段重命名、嵌套层级变化都会让客户端解析失败,这不是版本升级,是接口重做
记录
的访问频率,设个告警阈值,过期版本该下线就下线,别留着当“历史文物”
Header 和 Query 同时存在时以哪个为准?
有些团队为了“兼容性”,允许客户端既传
,又加
。这等于主动放弃一致性,调试时永远不知道当前走的是哪条分支。
必须明确优先级,并在文档和日志里体现:
我们定的规则是:Header > Query > 默认版本(
)
中间件里一旦从 Header 解析出有效版本,就忽略 Query 参数,且不记录 warning;但如果 Header 无效、Query 有效,则记录一条
日志
测试时重点覆盖边界情况:空 Header、非法格式(如
)、大小写混用(
)、多个
值(
)
别忘了检查
在 Nginx/FastCGI 下是否被截断或转小写——某些配置会强制 lower-case 所有 header
事情说清了就结束。版本控制最难的不是怎么写,而是所有人对“什么算 breaking change”有共识,以及敢删掉没人用的旧版本。
Accept/v1/usersAcceptAccept: application/vnd.myapp.v2+jsonv3v1/api/usersAccept$app->get('/v1/users')$app->get('/api/users')Acceptvnd.vnd.myapp.v1+jsonRouter::group()$request->header('Accept')routes/api.phpRoute::middleware(['parse.version'])->group(function () { ... });$request->attributes->set('api_version', 'v2')str_contains()preg_match()openapi.yamlAcceptv1/v1//v2/$versionUserResponseV1StrategyUserResponseV2Strategy/v1/v2/v1Accept: v2?version=v2v1deprecated_query_version_usedv-1V2Accepttext/html, application/vnd.myapp.v2+json$_SERVER['HTTP_ACCEPT']