本文目录导读:

- 方案一:URL路径版本(最常用,推荐)
- 方案二:请求头版本(适合内部API或移动端)
- 方案三:参数版本(简单直接,适合快速原型)
- 方案四:服务抽象 + 适配器模式(进阶,彻底根除冗余)
- 关键的最佳实践(平滑迭代的核心)
- 总结:你的项目该选哪个?
在PHP项目中实现接口版本的平滑迭代,核心目标是新老版本共存,互不影响,逐步迁移,以下是几种主流且成熟的实践方案,从简单到复杂,按项目体量和需求选择。
URL路径版本(最常用,推荐)
在URL中直接包含版本号,/api/v1/users 和 /api/v2/users。
实现方式:
-
目录结构:
controllers/ ├── v1/ │ └── UserController.php └── v2/ └── UserController.php -
路由配置(以 Laravel 为例):
// routes/api.php Route::prefix('v1')->group(function () { Route::get('users', [V1\UserController::class, 'index']); }); Route::prefix('v2')->group(function () { Route::get('users', [V2\UserController::class, 'index']); });
优点:
- 直观:版本号一目了然,便于调试和缓存。
- 使用最广:GitHub、Twitter 等都在用。
- 实现简单:无需解析 HTTP Header,Nginx/Apache 即可直接路由。
缺点:
- URL 不够优雅,v1->v2 后,旧 URL 仍需保留。
- 当版本过多时,代码重复度可能较高。
适用场景: 绝大多数项目,尤其是外界公开 API。
请求头版本(适合内部API或移动端)
通过 Accept 或自定义 Header(如 X-API-Version)来区分版本。
使用 Accept Header(RESTful 标准)
// 客户端请求
Accept: application/vnd.myapp.v1+json
// 服务端处理 (php)
public function index(Request $request) {
$version = $request->header('Accept');
// 解析版本号,调用对应的 Service/Controller
if (str_contains($version, 'v2')) {
return $this->handleV2();
}
return $this->handleV1();
}
使用自定义 Header(更简单)
// 客户端请求
X-API-Version: 2
// 服务端
$version = $request->header('X-API-Version', 1);
优点:
- URL 纯净:
/api/users保持不变,前端无需修改 URL。 - 更符合 RESTful 设计:资源不因版本而变。
缺点:
- 调试不直观:浏览器直接访问、Curl 测试需手动添加 Header。
- 缓存困难:CDN 或 HTTP 缓存难以识别不同 Header 版本。
适用场景: 内部微服务、BFF(Backend For Frontend)、移动端 App 场景(App 升级时 Header 自动带版本)。
参数版本(简单直接,适合快速原型)
在请求参数中带上 version。
GET /api/users?version=2
POST /api/users?version=2
实现:
$version = $_GET['version'] ?? 1;
优点: 实现最简单,浏览器可直接测试。
缺点:
- 滥用语义:版本信息应属于接口元数据,而非业务参数。
- 路由混乱:不能直接通过 Nginx 做版本缓存或限流。
- 安全性差:参数容易被篡改或忽略。
适用场景: 小型项目、内部测试工具、临时快速迭代。
服务抽象 + 适配器模式(进阶,彻底根除冗余)
核心思想:控制层(Controller)不做版本判断,将业务逻辑抽象成“接口”,不同版本实现继承或实现同一接口,由工厂类根据版本号实例化。
架构示意:
-
定义业务接口(Interface):
UserServiceInterface.phpgetUsers(int $page, int $size): array
-
不同版本实现:
UserServiceV1.php:实现旧逻辑(如返回['name','age'])UserServiceV2.php:实现新逻辑(如返回['fullName','birthday','phone'])
-
版本工厂:
class UserServiceFactory { public static function create(int $version): UserServiceInterface { return match($version) { 2 => new UserServiceV2(), default => new UserServiceV1(), }; } } -
使用(在 Controller 或 Middleware):
$version = $request->getApiVersion(); // 从 Header/URL 解析 $service = UserServiceFactory::create($version); $users = $service->getUsers($page, $size);
优点:
- 代码复用:V2 可以在 V1 基础上只修改差异部分。
- 易于测试:可以针对不同版本实例单独写单元测试。
- 版本共存、下线:只需修改工厂,不破坏已有路由。
缺点: 需要额外架构设计,复杂度较高。
适用场景: 大型项目,接口版本多、逻辑复杂、团队分工明确。
关键的最佳实践(平滑迭代的核心)
-
设好版本淘汰机制(Sunset Header)
旧版本用SunsetHTTP Header 提前告知调用方:HTTP/1.1 200 OK Sunset: Sat, 01 Jan 2022 23:59:59 GMT Deprecation: true -
默认兼容旧版本
不要求所有调用方立即升级,新版本增加新字段,而非修改老字段名或类型(除非语义明确错误)。 -
版本控制纳入 CI/CD
确保合并到主分支时,至少覆盖 v1 和 v2 的冒烟测试。 -
使用中间件统一版本解析
创建ApiVersionMiddleware,将版本号注入到 Request 实例中,避免每个 Controller 重复解析。 -
避免频繁升级大版本
通过向后兼容地添加新字段(可选) 替代新版本。- 旧响应:
{ "name": "Tom" } - 新响应:
{ "name": "Tom", "nick_name": "Tommy" }(旧客户端能自动忽略新字段)
- 旧响应:
-
文档联动
使用 OpenAPI(Swagger)为每个版本生成独立文档,版本号变化时自动更新文档。
你的项目该选哪个?
| 项目类型 | 推荐方案 |
|---|---|
| 小型初创项目,API 外部调用少 | 参数版本 或 URL路径版本 |
| 大型公开 API(如云服务、电商平台) | URL路径版本 + 适配器模式 |
| 微服务内部 RPC 或 BFF | 请求头版本 + 策略模式 |
| 移动端 App,版本迭代频繁 | URL路径版本 + 版本淘汰 Header |
最终建议: 对于 PHP 项目,推荐 URL路径版本 + 适配器模式,它平衡了简单性、实用性和可维护性,是多数大厂 PHP 项目的选择(如 Laravel 社区常见的做法)。