本文目录导读:

- 策略一:版本化路由(最推荐,适用于对外API)
- 策略二:适配器模式(适用于内部模块重构)
- 策略三:参数膨胀(适用于非关键字段变更)
- 策略四:特性开关(灰度发布,适用于渐进式上线)
- 策略五:中间件/切面兼容(处理数据格式差异)
- 策略六:数据迁移与双写(适用于数据库结构变更)
- 策略七:接口废弃流程(SDK 与文档)
- 总结选型建议
在PHP项目业务迭代中,兼容旧版接口是一个经典且重要的工程问题,核心原则是:“新增不破坏,修改需兼容,废弃有周期,下线有预案”。
以下是几种主流的兼容策略,按推荐程度从高到低排列:
版本化路由(最推荐,适用于对外API)
这是最规范、最常用的方式,通过在URL或请求头中携带版本号,让不同版本的接口并存。
URL路径版本化
// 路由配置示例(Laravel / Slim / ThinkPHP)
Route::prefix('v1')->group(function () {
Route::post('/order/create', 'V1\OrderController@create');
});
Route::prefix('v2')->group(function () {
Route::post('/order/create', 'V2\OrderController@create');
});
- 优点:清晰、直观、代码物理隔离,旧版逻辑完全不动。
- 缺点:代码重复度高(旧逻辑和旧模型仍需保留)。
请求头版本化
// 读取 Header 中的 Accept-Version
$version = $_SERVER['HTTP_ACCEPT_VERSION'] ?? 'v1';
if ($version === 'v2') {
// 新版逻辑
} else {
// 旧版逻辑(默认)
}
- 优点:URL 不变,更符合 RESTful 风格(资源标识不应该变)。
- 缺点:调试不够直观,旧逻辑依然与新版混在同一文件。
适用场景:对外公开的 API(如 SaaS 服务、App 后端)。
适配器模式(适用于内部模块重构)
当业务逻辑发生较大变化,但不想让调用方感知时,使用适配器将旧接口的调用转换为新接口。
// 旧接口(不可修改,或已大量调用)
class OldOrderService {
public function processOrder($userId, $productId, $price) {
// 旧逻辑
}
}
// 新接口(优化后的)
class NewOrderService {
public function handle(array $orderData) {
// 新逻辑
}
}
// 适配器(向下兼容)
class OrderServiceAdapter extends OldOrderService {
private $newService;
public function __construct() {
$this->newService = new NewOrderService();
}
public function processOrder($userId, $productId, $price) {
// 将旧参数转换成新参数
$orderData = [
'user_id' => $userId,
'product_id' => $productId,
'amount' => $price,
// ... 补充缺失字段
];
return $this->newService->handle($orderData);
}
}
适用场景:内部类库重构、数据库表结构变更后的数据访问层。
参数膨胀(适用于非关键字段变更)
当旧接口需要新增可选参数时,采用默认值策略,保证旧调用方不传参时行为不变。
// 旧调用方式: getUserInfo('abc123')
// 新调用方式: getUserInfo('abc123', ['include_avatar' => true])
function getUserInfo(string $userId, array $extra = []) {
$includeAvatar = $extra['include_avatar'] ?? false; // 默认不包含
// 核心逻辑不变
if ($includeAvatar) {
// 新增行为
}
}
优点:零成本兼容,改动最小。 缺点:函数签名会失控(参数太多、太复杂)。
特性开关(灰度发布,适用于渐进式上线)
通过配置开关,让新旧逻辑能在同一个代码版本下切换。
// config/feature.php
return [
'order.dual_region_discount' => false, // 旧逻辑
'order.dual_region_discount' => true, // 新逻辑
];
// 业务代码
if (Feature::isEnabled('order.dual_region_discount')) {
// 新逻辑(例如双区折扣)
} else {
// 旧逻辑
}
优点:可以针对特定用户、地区、比例控制,适合灰度。 缺点:代码中会出现大量 if/else,长期维护成本高,需及时清理。
中间件/切面兼容(处理数据格式差异)
当旧接口返回的字段名、格式与新版不一致时,在输出前使用中间件统一转换。
// Middleware(Laravel 示例)
public function handle($request, Closure $next) {
$response = $next($request);
// 如果是 v1 接口,对返回数据做格式转换
if ($request->route()->getPrefix() === 'v1') {
$data = $response->getData();
$data = $this->convertV2ToV1($data);
$response->setData($data);
}
return $response;
}
优点:业务代码中可以统一使用新版数据结构。 缺点:转换逻辑写起来很繁琐,容易漏转字段。
数据迁移与双写(适用于数据库结构变更)
如果业务迭代需要修改数据库表结构(如新增字段、拆分表),必须保证旧版接口仍能读写旧数据。
- 新增字段必须允许为 NULL 或有默认值。
- 读操作:统一查询,若字段为空则按旧逻辑处理。
- 写操作:对新旧接口分别处理,或使用双写(写新表同时写旧表)直至旧接口废弃。
-- 新增字段 必须可为空 ALTER TABLE orders ADD COLUMN delivery_type VARCHAR(10) NULL DEFAULT NULL;
接口废弃流程(SDK 与文档)
当决定废弃某个旧接口时,不是直接删除,而是执行标准退役流程:
- 标记废弃:响应头加
X-Deprecated: true,响应体加warning字段。 - 通知周期:至少提前 3~6个月 通知客户端(通过公告、邮件、日志)。
- 405/501 过渡:旧接口返回
405 Method Not Allowed,但不立即删除,给客户缓冲期。 - 下线前:最后检查调用量,确认零调用后清除代码。
总结选型建议
| 迭代场景 | 推荐策略 |
|---|---|
| 对外公开API(App/支付/第三方) | 策略一(版本化路由) |
| 内部服务重构(数据库/类库) | 策略二(适配器模式)+ 策略六(双写) |
| 小功能升级(新增可选参数) | 策略三(参数膨胀) |
| 高风险重大变更 | 策略四(特性开关) + A/B测试 |
| 数据格式不对齐 | 策略五(中间件转换) |
| 接口最终下线 | 策略七(标准退役流程) |
一句话建议:尽量用版本化路由,因为它最安全、易回溯,实在来不及就先用适配器撑住,后续再补版本化。永远不要在同一行代码里直接修改旧接口的返回值或行为,除非你能确认100%的调用方都已经升级。