Eloquent:API 资源
简介
构建 API 时,你可能需要在 Eloquent 模型与实际返回给应用用户的 JSON 响应之间加入一层转换。例如,你可能希望只对部分用户显示某些属性,或始终在模型的 JSON 表示中包含某些关联。Eloquent 的资源类让你能以富有表现力且简便的方式,将模型与模型集合转换为 JSON。
当然,你始终可以使用 toJson 方法将 Eloquent 模型或集合转换为 JSON;但 Eloquent 资源能对模型及其关联的 JSON 序列化提供更精细、更稳健的控制。
生成资源
要生成资源类,可使用 make:resource Artisan 命令。默认情况下,资源会放在应用的 app/Http/Resources 目录中。资源继承 Illuminate\Http\Resources\Json\JsonResource 类:
php artisan make:resource UserResource资源集合
除了生成转换单个模型的资源外,还可以生成负责转换模型集合的资源。这样 JSON 响应便可包含与给定资源整份集合相关的链接及其他元信息。
要创建资源集合,应在创建资源时使用 --collection 标志。或者,在资源名中包含单词 Collection 也会提示 Laravel 创建集合资源。集合资源继承 Illuminate\Http\Resources\Json\ResourceCollection 类:
php artisan make:resource User --collection
php artisan make:resource UserCollection概念概览
INFO
这是资源与资源集合的高层概览。强烈建议阅读本文档的其他章节,以便更深入地了解资源所提供的自定义能力与强大功能。
在深入编写资源时的全部选项之前,我们先从高层了解资源在 Laravel 中的用法。资源类表示需要转换为 JSON 结构的单个模型。例如,下面是一个简单的 UserResource 资源类:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
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,
];
}
}每个资源类都定义 toArray 方法,返回在资源作为路由或控制器方法的响应返回时应转换为 JSON 的属性数组。
注意,我们可以直接通过 $this 变量访问模型属性。这是因为资源类会自动将属性与方法访问代理到底层模型,以便使用。定义资源后,可从路由或控制器返回。资源通过构造函数接受底层模型实例:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/user/{id}', function (string $id) {
return new UserResource(User::findOrFail($id));
});
资源集合
若返回资源集合或分页响应,应在路由或控制器中创建资源实例时使用资源类提供的 collection 方法:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/users', function () {
return UserResource::collection(User::all());
});
请注意,这不允许添加可能需要随集合返回的任何自定义元数据。若希望自定义资源集合响应,可创建专用资源来表示该集合:
php artisan make:resource UserCollection生成资源集合类后,可轻松定义应随响应包含的任何元数据:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<int|string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'links' => [
'self' => 'link-value',
],
];
}
}定义资源集合后,可从路由或控制器返回:
use App\Http\Resources\UserCollection;
use App\Models\User;
Route::get('/users', function () {
return new UserCollection(User::all());
});
保留集合键
从路由返回资源集合时,Laravel 会重置集合的键,使其按数字顺序排列。不过,你可以在资源类上添加 preserveKeys 属性,以指示是否应保留集合的原始键:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Indicates if the resource's collection keys should be preserved.
*
* @var bool
*/
public $preserveKeys = true;
}当 preserveKeys 属性为 true 时,从路由或控制器返回集合时会保留集合键:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/users', function () {
return UserResource::collection(User::all()->keyBy->id);
});
自定义底层资源类
通常,资源集合的 $this->collection 属性会自动填充为:将集合中每一项映射到其单数资源类的结果。单数资源类假定为去掉类名末尾 Collection 后的集合类名。此外,按个人偏好,单数资源类可以带或不带 Resource 后缀。
例如,UserCollection 会尝试将给定的用户实例映射到 UserResource 资源。若要自定义此行为,可以覆盖资源集合的 $collects 属性:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* The resource that this resource collects.
*
* @var string
*/
public $collects = Member::class;
}编写资源
INFO
若尚未阅读概念概览,强烈建议在继续阅读本文档之前先阅读该部分。
资源只需将给定模型转换为数组。因此,每个资源都包含 toArray 方法,将模型属性转换为可从应用路由或控制器返回的、对 API 友好的数组:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
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,
];
}
}定义资源后,可直接从路由或控制器返回:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/user/{id}', function (string $id) {
return new UserResource(User::findOrFail($id));
});
关联
若希望在响应中包含关联资源,可将其加入资源 toArray 方法返回的数组。本例中,我们使用 PostResource 资源的 collection 方法,将用户的博客文章加入资源响应:
use App\Http\Resources\PostResource;
use Illuminate\Http\Request;
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'posts' => PostResource::collection($this->posts),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}INFO
若希望仅在关联已加载时才包含它们,请参阅条件关联文档。
资源集合
资源将单个模型转换为数组,而资源集合将模型集合转换为数组。不过,不必为每个模型都定义资源集合类,因为所有 Eloquent 模型集合都提供 toResourceCollection 方法,可即时生成「临时」资源集合:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/users', function () {
return UserResource::collection(User::all());
});
不过,若需要自定义随集合返回的元数据,则有必要定义自己的资源集合:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'links' => [
'self' => 'link-value',
],
];
}
}与单数资源一样,资源集合可直接从路由或控制器返回:
use App\Http\Resources\UserCollection;
use App\Models\User;
Route::get('/users', function () {
return new UserCollection(User::all());
});
数据包装
默认情况下,资源响应转换为 JSON 时,最外层资源会包装在 data 键中。因此,典型的资源集合响应类似如下:
{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "therese28@example.com"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "evandervort@example.com"
}
]
}若希望禁用最外层资源的包装,应在基类 Illuminate\Http\Resources\Json\JsonResource 上调用 withoutWrapping 方法。通常应在 AppServiceProvider 或每次请求都会加载的其他服务提供者中调用该方法:
<?php
namespace App\Providers;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
// ...
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
JsonResource::withoutWrapping();
}
}WARNING
withoutWrapping 方法只影响最外层响应,不会移除你在自己的资源集合中手动添加的 data 键。
包装嵌套资源
你可以完全自由地决定如何包装资源的关联。若希望无论嵌套层级如何,所有资源集合都包装在 data 键中,应为每个资源定义资源集合类,并在 data 键中返回集合。
你可能会担心这是否会导致最外层资源被包在两层 data 键中。不必担心,Laravel 不会让资源被意外双重包装,因此无需担心正在转换的资源集合的嵌套层级:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CommentsCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return ['data' => $this->collection];
}
}数据包装与分页
通过资源响应返回分页集合时,即使已调用 withoutWrapping,Laravel 仍会将资源数据包装在 data 键中。这是因为分页响应始终包含带有分页器状态信息的 meta 与 links 键:
{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "therese28@example.com"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "evandervort@example.com"
}
],
"links":{
"first": "http://example.com/users?page=1",
"last": "http://example.com/users?page=1",
"prev": null,
"next": null
},
"meta":{
"current_page": 1,
"from": 1,
"last_page": 1,
"path": "http://example.com/users",
"per_page": 15,
"to": 10,
"total": 10
}
}分页
你可以将 Laravel 分页器实例传给资源的 collection 方法或自定义资源集合:
use App\Http\Resources\UserCollection;
use App\Models\User;
Route::get('/users', function () {
return new UserCollection(User::paginate());
});
分页响应始终包含带有分页器状态信息的 meta 与 links 键:
{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "therese28@example.com"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "evandervort@example.com"
}
],
"links":{
"first": "http://example.com/users?page=1",
"last": "http://example.com/users?page=1",
"prev": null,
"next": null
},
"meta":{
"current_page": 1,
"from": 1,
"last_page": 1,
"path": "http://example.com/users",
"per_page": 15,
"to": 10,
"total": 10
}
}自定义分页信息
若希望自定义分页响应中 links 或 meta 键包含的信息,可在资源上定义 paginationInformation 方法。该方法会收到 $paginated 数据以及包含 links 与 meta 键的 $default 信息数组:
/**
* Customize the pagination information for the resource.
*
* @param \Illuminate\Http\Request $request
* @param array $paginated
* @param array $default
* @return array
*/
public function paginationInformation($request, $paginated, $default)
{
$default['links']['custom'] = 'https://example.com';
return $default;
}条件属性
有时你可能希望仅在满足给定条件时才将属性包含在资源响应中。例如,仅当当前用户是「管理员」时才包含某个值。Laravel 为此提供了多种辅助方法。when 方法可用于有条件地向资源响应添加属性:
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'secret' => $this->when($request->user()->isAdmin(), 'secret-value'),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}本例中,仅当已认证用户的 isAdmin 方法返回 true 时,最终资源响应才会包含 secret 键。若返回 false,在发送给客户端之前会从资源响应中移除 secret。when 方法让你能以富有表现力的方式定义资源,而无需在构建数组时使用条件语句。
when 方法也接受闭包作为第二个参数,从而仅在给定条件为 true 时才计算结果值:
'secret' => $this->when($request->user()->isAdmin(), function () {
return 'secret-value';
}),
若属性确实存在于底层模型上,可使用 whenHas 方法包含该属性:
'name' => $this->whenHas('name'),
此外,若属性不为 null,可使用 whenNotNull 方法将其包含在资源响应中:
'name' => $this->whenNotNull($this->name),
合并条件属性
有时你可能有多个属性应基于同一条件才包含在资源响应中。此时可使用 mergeWhen 方法,仅在给定条件为 true 时将这些属性加入响应:
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
$this->mergeWhen($request->user()->isAdmin(), [
'first-secret' => 'value',
'second-secret' => 'value',
]),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}同样,若给定条件为 false,在发送给客户端之前会从资源响应中移除这些属性。
WARNING
mergeWhen 方法不应在混合字符串键与数字键的数组中使用。此外,也不应在数字键未按顺序排列的数组中使用。
条件关联
除了有条件地加载属性外,你还可以根据关联是否已在模型上加载,有条件地在资源响应中包含关联。这样控制器可决定应在模型上加载哪些关联,资源则仅在实际已加载时轻松包含它们。最终,这有助于避免资源中的「N+1」查询问题。
whenLoaded 方法可用于有条件地加载关联。为避免不必要地加载关联,该方法接受关联名称而非关联本身:
use App\Http\Resources\PostResource;
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'posts' => PostResource::collection($this->whenLoaded('posts')),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}本例中,若关联尚未加载,在发送给客户端之前会从资源响应中移除 posts 键。
条件关联计数
除了有条件地包含关联外,还可根据关联计数是否已在模型上加载,有条件地在资源响应中包含关联「计数」:
new UserResource($user->loadCount('posts'));
whenCounted 方法可用于有条件地将关联计数包含在资源响应中。若关联计数不存在,该方法可避免不必要地包含该属性:
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'posts_count' => $this->whenCounted('posts'),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}本例中,若 posts 关联的计数尚未加载,在发送给客户端之前会从资源响应中移除 posts_count 键。
其他类型的聚合(如 avg、sum、min、max)也可使用 whenAggregated 方法有条件地加载:
'words_avg' => $this->whenAggregated('posts', 'words', 'avg'),
'words_sum' => $this->whenAggregated('posts', 'words', 'sum'),
'words_min' => $this->whenAggregated('posts', 'words', 'min'),
'words_max' => $this->whenAggregated('posts', 'words', 'max'),条件中间表信息
除了有条件地在资源响应中包含关联信息外,还可使用 whenPivotLoaded 方法有条件地包含多对多关联中间表中的数据。whenPivotLoaded 的第一个参数为中间表名,第二个参数应为闭包:若模型上有中间表信息则返回相应值:
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'expires_at' => $this->whenPivotLoaded('role_user', function () {
return $this->pivot->expires_at;
}),
];
}若关联使用自定义中间表模型,可将中间表模型实例作为第一个参数传给 whenPivotLoaded:
'expires_at' => $this->whenPivotLoaded(new Membership, function () {
return $this->pivot->expires_at;
}),
若中间表使用的访问器不是 pivot,可使用 whenPivotLoadedAs 方法:
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'expires_at' => $this->whenPivotLoadedAs('subscription', 'role_user', function () {
return $this->subscription->expires_at;
}),
];
}添加元数据
某些 JSON API 标准要求在资源与资源集合响应中添加元数据。这通常包括指向该资源或相关资源的 links,或关于资源本身的元数据。若需要返回资源的额外元数据,请将其包含在 toArray 方法中。例如,转换资源集合时可以包含 links 信息:
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'links' => [
'self' => 'link-value',
],
];
}从资源返回额外元数据时,不必担心会意外覆盖 Laravel 在返回分页响应时自动添加的 links 或 meta 键。你定义的任何额外 links 都会与分页器提供的链接合并。
顶层元数据
有时你可能希望仅当资源是返回的最外层资源时,才在资源响应中包含某些元数据。这通常包括关于整个响应的元信息。要定义此类元数据,请在资源类上添加 with 方法。该方法应返回仅在该资源为正在转换的最外层资源时才随资源响应包含的元数据数组:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
/**
* Get additional data that should be returned with the resource array.
*
* @return array<string, mixed>
*/
public function with(Request $request): array
{
return [
'meta' => [
'key' => 'value',
],
];
}
}构造资源时添加元数据
你也可以在路由或控制器中构造资源实例时添加顶层数据。所有资源上都可用的 additional 方法接受应加入资源响应的数据数组:
return (new UserCollection(User::all()->load('roles')))
->additional(['meta' => [
'key' => 'value',
]]);
资源响应
如你已读到的,资源可直接从路由和控制器返回:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/user/{id}', function (string $id) {
return new UserResource(User::findOrFail($id));
});
不过,有时你可能需要在发送给客户端之前自定义出站 HTTP 响应。有两种方式。首先,可在资源上链式调用 response 方法。该方法返回 Illuminate\Http\JsonResponse 实例,让你完全控制响应头:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/user', function () {
return (new UserResource(User::find(1)))
->response()
->header('X-Value', 'True');
});
或者,可在资源本身中定义 withResponse 方法。当资源作为响应中的最外层资源返回时会调用该方法:
<?php
namespace App\Http\Resources;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
];
}
/**
* Customize the outgoing response for the resource.
*/
public function withResponse(Request $request, JsonResponse $response): void
{
$response->header('X-Value', 'True');
}
}