什么是 API 资源
在构建 API 时,如果直接把 Eloquent 模型作为 JSON 返回,可能会泄露本该隐藏的字段,或者向客户端发送大量它并不需要的数据。
Eloquent API 资源在模型与 JSON 响应之间引入了一层转换层。可以通过 toArray() 方法明确地定义“响应里包含哪些内容,以什么格式呈现”。
主要好处包括:
- 完全控制响应中包含的字段
- 字段重命名、值加工可以集中在一个位置
- 可以根据条件决定字段是否输出
- 可以嵌套关联资源,保持结构一致
创建资源
使用 make:resource Artisan 命令生成资源类。
php artisan make:resource UserResource
生成的类会被放到 app/Http/Resources 目录。
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
可以通过 $this 直接访问模型的属性。这是因为资源类内部会把访问代理到模型上。
在控制器中使用
定义好的资源可以直接从控制器或路由中返回。
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/users/{user}', function (User $user) {
return new UserResource($user);
});
或者使用模型的 toResource() 方法。
Route::get('/users/{user}', function (User $user) {
return $user->toResource();
});
toResource() 会根据模型名自动查找对应的资源类(UserResource)。
默认情况下,响应会被包装在 data 键下。
{
"data": {
"id": 1,
"name": "山田 太郎",
"email": "[email protected]",
"created_at": "2024-01-15T10:00:00.000000Z",
"updated_at": "2024-01-15T10:00:00.000000Z"
}
}
资源集合
返回多个模型时使用 collection() 方法。
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/users', function () {
return UserResource::collection(User::all());
});
或者使用 Eloquent 集合的 toResourceCollection()。
return User::all()->toResourceCollection();
自定义集合资源
如果要给整个集合附加元数据,可以创建专门的集合资源。
php artisan make:resource UserCollection
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'links' => [
'self' => route('users.index'),
],
];
}
}
Route::get('/users', function () {
return new UserCollection(User::all());
});
字段的加工与转换
toArray() 中可以修改字段名或对值进行加工。
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'full_name' => $this->name, // 更换字段名
'email_address' => $this->email, // 更换字段名
'role' => strtoupper($this->role), // 值加工
'registered_at' => $this->created_at->toDateString(), // 格式化日期
];
}
条件字段
when() — 根据条件添加字段
如果只在特定条件下才包含某个字段,可以使用 when()。
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
// 只在当前登录用户是管理员时输出
'secret_token' => $this->when(
$request->user()?->isAdmin(),
$this->secret_token
),
];
}
当 when() 的条件为 false 时,整个键都会从响应中移除。
mergeWhen() — 一次性有条件地添加多个字段
若要按同一个条件批量决定多个字段是否输出,可以使用 mergeWhen()。
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
$this->mergeWhen($request->user()?->isAdmin(), [
'admin_note' => $this->admin_note,
'internal_id' => $this->internal_id,
]),
];
}
whenLoaded() — 只包含已加载的关联
只在关联已经被 Eager Load 时才包含它,可以避免 N+1 问题,同时保持响应的灵活性。
use App\Http\Resources\PostResource;
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
// 只有当 posts 通过 with() 加载时才包含
'posts' => PostResource::collection($this->whenLoaded('posts')),
];
}
由控制器负责决定是否加载关联。
// 带上 posts 返回
return new UserResource($user->load('posts'));
// 不带 posts 返回
return new UserResource($user);
whenCounted() — 条件性包含计数
用来在响应中有条件地包含通过 loadCount() 得到的关联计数。
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'posts_count' => $this->whenCounted('posts'),
];
}
return new UserResource($user->loadCount('posts'));
嵌套资源
通过在关联上嵌套另一个资源类,可以保持响应结构一致。
// app/Http/Resources/PostResource.php
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'body' => $this->body,
'author' => new UserResource($this->whenLoaded('user')),
'comments' => CommentResource::collection($this->whenLoaded('comments')),
'published_at' => $this->published_at?->toDateString(),
];
}
}
// 控制器
$post = Post::with(['user', 'comments'])->findOrFail($id);
return new PostResource($post);
{
"data": {
"id": 1,
"title": "用 Laravel 学习 API 资源",
"author": {
"id": 5,
"name": "山田 太郎",
"email": "[email protected]"
},
"comments": [
{ "id": 10, "body": "非常有帮助!" }
]
}
}
添加元数据
with() — 顶层元数据
若要给整个集合附加元数据,可以重写 with() 方法。
class UserCollection extends ResourceCollection
{
public function toArray(Request $request): array
{
return parent::toArray($request);
}
public function with(Request $request): array
{
return [
'meta' => [
'version' => '1.0',
'generated_at' => now()->toIso8601String(),
],
];
}
}
响应示例:
{
"data": [...],
"meta": {
"version": "1.0",
"generated_at": "2024-01-15T10:00:00+00:00"
}
}
additional() — 动态添加元数据
如果需要在控制器侧动态添加元数据,可以使用 additional()。
return User::all()
->load('roles')
->toResourceCollection()
->additional(['meta' => [
'total_admins' => User::where('role', 'admin')->count(),
]]);
与分页结合
把分页结果直接传给资源,就会自动附上 meta 和 links。
Route::get('/users', function () {
return UserResource::collection(User::paginate(15));
});
或:
return User::paginate(15)->toResourceCollection();
响应示例:
{
"data": [
{ "id": 1, "name": "山田 太郎" },
{ "id": 2, "name": "鈴木 花子" }
],
"links": {
"first": "https://example.com/users?page=1",
"last": "https://example.com/users?page=5",
"prev": null,
"next": "https://example.com/users?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"per_page": 15,
"to": 15,
"total": 72
}
}
分页响应中即使调用了 withoutWrapping(),data 键也总是会附上,以便与分页的 meta 和 links 键共存。
禁用数据包装
默认情况下,最外层的资源会被包装在 data 键下。若要禁用,可以在 AppServiceProvider 的 boot() 中调用 withoutWrapping()。
use Illuminate\Http\Resources\Json\JsonResource;
public function boot(): void
{
JsonResource::withoutWrapping();
}
withoutWrapping() 只影响最外层的包装,你自己定义的 data 键不会被移除。
实战示例:用户 API 的实现
下面以用户管理 API 为例,展示如何使用资源来设计一致的响应。
UserResource
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'avatar_url' => $this->avatar_url,
'role' => $this->role,
// 只有管理员才可见
'created_at' => $this->when(
$request->user()?->isAdmin(),
$this->created_at->toDateString()
),
// 仅在关联已加载时包含
'posts' => PostResource::collection($this->whenLoaded('posts')),
'posts_count' => $this->whenCounted('posts'),
];
}
}
UserController
<?php
namespace App\Http\Controllers\Api;
use App\Http\Resources\UserResource;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
class UserController extends Controller
{
public function index(): AnonymousResourceCollection
{
$users = User::withCount('posts')->paginate(20);
return UserResource::collection($users);
}
public function show(User $user): UserResource
{
$user->loadCount('posts')->load('posts');
return new UserResource($user);
}
}
相关页面
Eloquent 关联入门
了解如何定义关联并使用预加载。