API 版本控制策略
API 上线后必然需要迭代。不加版本控制的 API,任何 Breaking Change 都可能让已有客户端崩溃。本节介绍 Laravel 中最实用的 URL 路径版本化方案,以及如何通过 Resource 继承优雅地管理版本差异。
版本控制策略对比
| 方案 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| URL 路径版本 | /api/v1/tasks | 直观,易调试,浏览器可见 | URL 冗长 |
| Header 版本 | Accept: application/vnd.api+json;version=1 | 干净的 URL | 调试不方便 |
| Query 参数 | /api/tasks?version=1 | 简单 | 非语义化,缓存难 |
推荐:URL 路径版本化(最直观,前端/移动端接入最简单)
目录结构设计
app/Http/
├── Controllers/
│ └── Api/
│ ├── V1/
│ │ ├── TaskController.php
│ │ └── UserController.php
│ └── V2/
│ └── TaskController.php ← 只重写有变化的控制器
└── Resources/
└── Api/
├── V1/
│ ├── TaskResource.php
│ └── UserResource.php
└── V2/
└── TaskResource.php ← 继承 V1,只覆盖变化的字段
routes/
└── api.php
路由分组
// routes/api.php
use App\Http\Controllers\Api;
// V1
Route::prefix('v1')
->name('api.v1.')
->middleware(['auth:sanctum', 'throttle:api'])
->group(function () {
Route::apiResource('tasks', Api\V1\TaskController::class);
Route::apiResource('users', Api\V1\UserController::class);
Route::apiResource('projects', Api\V1\ProjectController::class);
});
// V2(只定义有变化的路由)
Route::prefix('v2')
->name('api.v2.')
->middleware(['auth:sanctum', 'throttle:api'])
->group(function () {
// V2 的 Task 响应格式变了
Route::apiResource('tasks', Api\V2\TaskController::class);
// users 和 projects 没有变化,仍指向 V1 控制器
Route::apiResource('users', Api\V1\UserController::class);
Route::apiResource('projects', Api\V1\ProjectController::class);
});
Resource 版本继承
<?php
// app/Http/Resources/Api/V1/TaskResource.php
namespace App\Http\Resources\Api\V1;
use Illuminate\Http\Resources\Json\JsonResource;
class TaskResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'status' => $this->status,
'user' => new UserResource($this->whenLoaded('user')),
];
}
}
// app/Http/Resources/Api/V2/TaskResource.php
// V2 继承 V1,只修改变化的部分
namespace App\Http\Resources\Api\V2;
use App\Http\Resources\Api\V1\TaskResource as V1TaskResource;
class TaskResource extends V1TaskResource
{
public function toArray($request): array
{
$data = parent::toArray($request);
// V2 变化:重命名字段
$data['task_status'] = $data['status']; // status → task_status
unset($data['status']);
// V2 新增字段
$data['priority_label'] = match ($this->priority) {
5 => 'critical',
4 => 'high',
3 => 'medium',
default => 'low',
};
// V2 使用新的 User Resource 格式
$data['assignee'] = new UserResource($this->whenLoaded('user'));
unset($data['user']);
return $data;
}
}
版本废弃通知
// 在旧版本控制器中添加 Deprecation 响应头
// app/Http/Controllers/Api/V1/TaskController.php
class TaskController extends Controller
{
public function index()
{
$tasks = Task::with('user')->paginate(20);
return TaskResource::collection($tasks)
->response()
->header('Deprecation', 'Sun, 01 Jan 2025 00:00:00 GMT') // RFC 8594
->header('Sunset', 'Sun, 01 Jun 2025 00:00:00 GMT') // 关闭日期
->header('Link', '<https://taskflow.app/api/v2/tasks>; rel="successor-version"');
}
}
// 或者用中间件统一添加:
class AddApiDeprecationHeader
{
public function handle(Request $request, Closure $next): mixed
{
$response = $next($request);
if (str_starts_with($request->path(), 'api/v1')) {
$response->headers->set('Deprecation', 'true');
$response->headers->set('Sunset', 'Sun, 01 Jun 2025 00:00:00 GMT');
}
return $response;
}
}
API 错误响应规范化
// app/Exceptions/Handler.php(或 bootstrap/app.php)
// 统一 API 错误格式
->withExceptions(function (Exceptions $exceptions) {
// 未认证
$exceptions->render(function (\Illuminate\Auth\AuthenticationException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json([
'error' => 'unauthenticated',
'message' => '请先登录',
], 401);
}
});
// 权限不足
$exceptions->render(function (\Illuminate\Auth\Access\AuthorizationException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json([
'error' => 'forbidden',
'message' => '权限不足',
], 403);
}
});
// 模型不存在(路由模型绑定找不到时)
$exceptions->render(function (\Illuminate\Database\Eloquent\ModelNotFoundException $e, Request $request) {
if ($request->expectsJson()) {
return response()->json([
'error' => 'not_found',
'message' => '资源不存在',
], 404);
}
});
})
下一节:分页、过滤与排序标准化——API 的列表接口需要标准化的分页、过滤和排序参数设计,让前端有一致的接口规范。