URL路径嵌入版本号(如/api/v1/users)最简单可靠,天然适配工具链与缓存机制,优于Header或查询参数方式;Laravel和ThinkPHP可通过路由前缀与变量路由实现版本隔离,并推荐按版本分目录、用服务工厂模式避免控制器内版本判断。

直接用 URL 路径嵌入版本号(如 /api/v1/users)是最简单、最可靠的方式,不需要额外解析请求头或参数,调试方便,框架支持成熟,90% 以上的 PHP 项目都该从这条路开始。
为什么选 URL 路径版本控制而不是 Header 或参数
URL 方式天然符合人类直觉和工具链习惯:Postman 点几下就能切版本,curl 直接改路径就能测,Nginx 日志里一眼看出哪个版本调用量大,CDN 缓存也按完整 URL 区分——不会因为 Accept 头不同却缓存错内容。而 X-API-Version 头要客户端配合、调试时容易漏设;?version=v1 则破坏 RESTful 原则,且会被代理、CDN、浏览器历史记录误判为不同资源,导致缓存失效或埋点错乱。
Laravel 和 ThinkPHP 的路由配置实操
在 Laravel 中,用 Route::group 配前缀和命名空间即可隔离逻辑:
Route::prefix('api/v1')->namespace('AppHttpControllersV1')->group(function () {
Route::get('/users', [UserController::class, 'index']);
});
ThinkPHP 5/6 推荐用变量路由,避免重复写一堆 v1/xxx、v2/xxx:
立即学习“PHP免费学习笔记(深入)”;
Route::group(['prefix' => 'api'], function () {
Route::rule(':version/<controller>/<action>', 'api/:version.:controller/:action')
->pattern(['version' => 'v[12]']); // 只匹配 v1 或 v2,防误触
});
-
:version会作为参数传入控制器,但不会自动注入方法,得用$this->request->param('version')拿 - 目录结构建议按
app/controller/v1/UserController.php和app/controller/v2/UserController.php分开,别塞进同一个类里 if-else - 命名空间必须和目录严格对应,否则
class_exists()检查会失败,线上报 500
控制器里怎么避免写满版本判断
拿到 :version 后,别在控制器里写 if ($version === 'v1') { ... } else { ... }。这样很快变成“版本判断器”,一加 v3 就要改所有接口。正确做法是工厂加载服务类:
$serviceClass = "App\Services\{$version}\UserService";
if (!class_exists($serviceClass)) {
abort(400, 'Unsupported API version');
}
$service = new $serviceClass();
- 每个版本的服务类实现同一接口(如
UserContract),保证方法签名一致 - 数据库字段变更时,v1 服务读
user_status,v2 服务读user_state并做映射,不改老表结构 - 响应格式统一走 Transformer 或 DTO,v1 返回
['id', 'name'],v2 返回['id', 'name', 'avatar_url'],但都由各自版本的UserResource控制
上线后最容易被忽略的三件事
版本控制不是配完路由就结束了。v1 上线半年后,你得确认:v1 接口还在被哪些小程序版本调用;user_status 字段还没被删,哪怕 v2 已用新字段;所有响应头里都带了 Sunset: Wed, 27 Sep 2026 00:00:00 GMT 提示废弃时间。这些细节没盯住,平滑过渡就是空话。



















