Laravel 13 API 版本控制完全指南
API 版本控制是构建可维护 API 的关键实践。本文将深入探讨 Laravel 13 中实现 API 版本控制的各种策略和最佳实践。
版本控制策略
URL 路径版本控制
1 2 3 4 5 6 7 8 9 10
| Route::prefix('v1')->group(function () { Route::get('/users', [Api\V1\UserController::class, 'index']); Route::get('/users/{id}', [Api\V1\UserController::class, 'show']); });
Route::prefix('v2')->group(function () { Route::get('/users', [Api\V2\UserController::class, 'index']); Route::get('/users/{id}', [Api\V2\UserController::class, 'show']); });
|
请求头版本控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php
namespace App\Http\Middleware;
use Closure; use Illuminate\Http\Request;
class ApiVersion { public function handle(Request $request, Closure $next) { $version = $request->header('Accept-Version', 'v1'); if (!in_array($version, ['v1', 'v2'])) { $version = 'v1'; } $request->attributes->set('api_version', $version); return $next($request); } }
|
内容协商版本控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php
namespace App\Http\Middleware;
use Closure; use Illuminate\Http\Request;
class ContentNegotiation { public function handle(Request $request, Closure $next) { $accept = $request->header('Accept'); if (preg_match('/application\/vnd\.api\.v(\d+)\+json/', $accept, $matches)) { $request->attributes->set('api_version', 'v' . $matches[1]); } else { $request->attributes->set('api_version', 'v1'); } return $next($request); } }
|
目录结构设计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| app/ ├── Http/ │ ├── Controllers/ │ │ └── Api/ │ │ ├── V1/ │ │ │ ├── UserController.php │ │ │ ├── PostController.php │ │ │ └── BaseController.php │ │ └── V2/ │ │ ├── UserController.php │ │ ├── PostController.php │ │ └── BaseController.php │ └── Resources/ │ ├── V1/ │ │ ├── UserResource.php │ │ └── PostResource.php │ └── V2/ │ ├── UserResource.php │ └── PostResource.php ├── Models/ │ ├── User.php │ └── Post.php └── Services/ ├── UserService.php └── PostService.php
|
路由组织
版本化路由文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| use App\Http\Controllers\Api\V1\UserController;
Route::middleware(['auth:sanctum'])->group(function () { Route::apiResource('users', UserController::class); Route::get('users/{user}/posts', [UserController::class, 'posts']); });
use App\Http\Controllers\Api\V2\UserController;
Route::middleware(['auth:sanctum'])->group(function () { Route::apiResource('users', UserController::class); Route::get('users/{user}/posts', [UserController::class, 'posts']); Route::get('users/{user}/statistics', [UserController::class, 'statistics']); });
|
路由服务提供者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\Facades\Route;
class ApiRouteServiceProvider extends ServiceProvider { public function boot(): void { Route::prefix('api/v1') ->middleware('api') ->namespace('App\Http\Controllers\Api\V1') ->group(base_path('routes/api/v1.php'));
Route::prefix('api/v2') ->middleware('api') ->namespace('App\Http\Controllers\Api\V2') ->group(base_path('routes/api/v2.php')); } }
|
控制器设计
基础控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller; use Illuminate\Http\JsonResponse;
abstract class BaseController extends Controller { protected function successResponse($data, string $message = '', int $code = 200): JsonResponse { return response()->json([ 'success' => true, 'data' => $data, 'message' => $message, ], $code); }
protected function errorResponse(string $message, int $code = 400): JsonResponse { return response()->json([ 'success' => false, 'message' => $message, ], $code); }
protected function paginatedResponse($paginator, string $resourceClass): JsonResponse { return response()->json([ 'success' => true, 'data' => $resourceClass::collection($paginator->items()), 'meta' => [ 'current_page' => $paginator->currentPage(), 'last_page' => $paginator->lastPage(), 'per_page' => $paginator->perPage(), 'total' => $paginator->total(), ], 'links' => [ 'first' => $paginator->url(1), 'last' => $paginator->url($paginator->lastPage()), 'prev' => $paginator->previousPageUrl(), 'next' => $paginator->nextPageUrl(), ], ]); } }
|
V1 控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| <?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Api\BaseController; use App\Http\Resources\V1\UserResource; use App\Models\User; use App\Http\Requests\V1\StoreUserRequest; use App\Http\Requests\V1\UpdateUserRequest;
class UserController extends BaseController { public function index() { $users = User::query() ->when(request('search'), fn($q, $search) => $q->where('name', 'like', "%{$search}%")) ->paginate(request('per_page', 15));
return $this->paginatedResponse($users, UserResource::class); }
public function show(User $user) { return $this->successResponse(new UserResource($user)); }
public function store(StoreUserRequest $request) { $user = User::create($request->validated()); return $this->successResponse(new UserResource($user), '用户创建成功', 201); }
public function update(UpdateUserRequest $request, User $user) { $user->update($request->validated()); return $this->successResponse(new UserResource($user), '用户更新成功'); }
public function destroy(User $user) { $user->delete(); return $this->successResponse(null, '用户删除成功'); } }
|
V2 控制器(扩展功能)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php
namespace App\Http\Controllers\Api\V2;
use App\Http\Controllers\Api\BaseController; use App\Http\Resources\V2\UserResource; use App\Models\User; use App\Http\Requests\V2\StoreUserRequest; use App\Services\UserService;
class UserController extends BaseController { public function __construct( protected UserService $userService ) {}
public function index() { $users = $this->userService->getUsersWithStatistics( request('search'), request('per_page', 15) );
return $this->paginatedResponse($users, UserResource::class); }
public function show(User $user) { $user->load(['profile', 'statistics']); return $this->successResponse(new UserResource($user)); }
public function statistics(User $user) { return $this->successResponse([ 'posts_count' => $user->posts()->count(), 'comments_count' => $user->comments()->count(), 'likes_received' => $user->likesReceived()->count(), ]); } }
|
API 资源
V1 资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <?php
namespace App\Http\Resources\V1;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource { public function toArray($request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'created_at' => $this->created_at->toISOString(), 'updated_at' => $this->updated_at->toISOString(), ]; } }
|
V2 资源(扩展字段)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <?php
namespace App\Http\Resources\V2;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource { public function toArray($request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'avatar' => $this->avatar_url, 'profile' => $this->whenLoaded('profile', fn() => [ 'bio' => $this->profile->bio, 'website' => $this->profile->website, 'location' => $this->profile->location, ]), 'statistics' => $this->whenLoaded('statistics', fn() => [ 'posts_count' => $this->statistics->posts_count, 'followers_count' => $this->statistics->followers_count, ]), 'links' => [ 'self' => route('api.v2.users.show', $this->id), 'posts' => route('api.v2.users.posts', $this->id), ], 'created_at' => $this->created_at->toISOString(), 'updated_at' => $this->updated_at->toISOString(), ]; } }
|
表单请求验证
V1 验证规则
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <?php
namespace App\Http\Requests\V1;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest { public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'unique:users,email'], 'password' => ['required', 'string', 'min:8'], ]; } }
|
V2 验证规则(扩展)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php
namespace App\Http\Requests\V2;
use Illuminate\Foundation\Http\FormRequest;
class StoreUserRequest extends FormRequest { public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'email', 'unique:users,email'], 'password' => ['required', 'string', 'min:8', 'confirmed'], 'avatar' => ['nullable', 'image', 'max:2048'], 'profile' => ['nullable', 'array'], 'profile.bio' => ['nullable', 'string', 'max:500'], 'profile.website' => ['nullable', 'url'], 'profile.location' => ['nullable', 'string', 'max:100'], ]; } }
|
版本废弃策略
废弃中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php
namespace App\Http\Middleware;
use Closure; use Illuminate\Http\Request;
class DeprecatedRoute { public function handle(Request $request, Closure $next, string $sunset = null) { $response = $next($request); $response->headers->set('X-API-Deprecated', 'true'); $response->headers->set('X-API-Sunset', $sunset ?? now()->addMonths(6)->toDateString()); $response->headers->set('Link', '<' . route('api.v2.users.index') . '>; rel="successor-version"'); return $response; } }
|
应用废弃中间件
1 2 3 4
| Route::prefix('v1')->middleware('deprecated:2025-12-31')->group(function () { Route::get('/users', [Api\V1\UserController::class, 'index']); });
|
版本切换中间件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <?php
namespace App\Http\Middleware;
use Closure; use Illuminate\Http\Request; use Illuminate\Routing\Route;
class ApiVersionSwitch { public function handle(Request $request, Closure $next) { $version = $this->resolveVersion($request); $controller = $request->route()->getController(); if (method_exists($controller, 'setVersion')) { $controller->setVersion($version); } return $next($request); }
protected function resolveVersion(Request $request): string { if ($version = $request->header('X-API-Version')) { return $version; }
if ($version = $request->query('version')) { return $version; }
if (preg_match('/\/api\/(v\d+)\//', $request->path(), $matches)) { return $matches[1]; }
return config('api.default_version', 'v1'); } }
|
API 文档
OpenAPI 规范
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| openapi: 3.0.0 info: title: Laravel API version: 2.0.0 servers: - url: /api/v2 description: V2 API - url: /api/v1 description: V1 API (Deprecated) paths: /users: get: summary: List users tags: - Users parameters: - name: search in: query schema: type: string - name: per_page in: query schema: type: integer default: 15 responses: '200': description: Success content: application/json: schema: $ref: '#/components/schemas/UserCollection'
|
测试版本化 API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| <?php
namespace Tests\Feature\Api;
use Tests\TestCase; use App\Models\User;
class UserApiTest extends TestCase { public function test_v1_users_index() { User::factory()->count(5)->create();
$response = $this->getJson('/api/v1/users');
$response->assertStatus(200) ->assertJsonStructure([ 'success', 'data' => [ '*' => ['id', 'name', 'email'] ], 'meta' => ['current_page', 'total'] ]); }
public function test_v2_users_index_with_statistics() { User::factory()->count(5)->create();
$response = $this->getJson('/api/v2/users');
$response->assertStatus(200) ->assertJsonStructure([ 'success', 'data' => [ '*' => ['id', 'name', 'email', 'avatar', 'statistics'] ] ]); }
public function test_version_via_header() { $response = $this->withHeaders(['X-API-Version' => 'v2']) ->getJson('/api/users');
$response->assertStatus(200); } }
|
总结
Laravel 13 的 API 版本控制策略包括:
- URL 路径版本控制(最常用)
- 请求头版本控制
- 内容协商版本控制
- 清晰的目录结构
- 版本废弃策略
- 完整的测试支持
选择合适的版本控制策略,可以确保 API 的长期可维护性和向后兼容性。