Laravel 13 API 版本控制完全指南

API 版本控制是构建可维护 API 的关键实践。本文将深入探讨 Laravel 13 中实现 API 版本控制的各种策略和最佳实践。

版本控制策略

URL 路径版本控制

1
2
3
4
5
6
7
8
9
10
// routes/api.php
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
// app/Http/Middleware/ApiVersion.php
<?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
// app/Http/Middleware/ContentNegotiation.php
<?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
// routes/api/v1.php
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']);
});

// routes/api/v2.php
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
// routes/api.php
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 的长期可维护性和向后兼容性。