跳至内容

Eloquent:API 资源

介绍

构建 API 时,您可能需要一个转换层,它位于 Eloquent 模型和实际返回给应用程序用户的 JSON 响应之间。例如,您可能希望显示部分用户的某些属性,而不显示其他用户,或者您可能希望始终在模型的 JSON 表示中包含某些关系。Eloquent 的资源类允许您以简洁明了的方式将模型和模型集合转换为 JSON。

当然,您可以随时使用它们的toJson方法将 Eloquent 模型或集合转换为 JSON;但是,Eloquent 资源对模型及其关系的 JSON 序列化提供了更精细、更强大的控制。

生成资源

要生成资源类,您可以使用make:resourceArtisan 命令。默认情况下,资源将放置在app/Http/Resources应用程序的目录中。资源扩展了以下Illuminate\Http\Resources\Json\JsonResource类:

1php artisan make:resource UserResource

资源集合

除了生成用于转换单个模型的资源外,您还可以生成用于转换模型集合的资源。这样,您的 JSON 响应就可以包含与给定资源的整个集合相关的链接和其他元信息。

要创建资源集合,您应该--collection在创建资源时使用标志。或者,在资源名称中包含该单词,Collection这将指示 Laravel 应该创建集合资源。集合资源扩展了以下Illuminate\Http\Resources\Json\ResourceCollection类:

1php artisan make:resource User --collection
2 
3php artisan make:resource UserCollection

概念概述

本文是对资源和资源集合的概述。强烈建议您阅读本文档的其他部分,以更深入地了解资源提供的自定义功能和强大功能。

在深入探讨编写资源时可用的所有选项之前,让我们先来大致了解一下 Laravel 中资源的使用方法。资源类表示需要转换为 JSON 结构的单个模型。例如,以下是一个简单的UserResource资源类:

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\Json\JsonResource;
7 
8class UserResource extends JsonResource
9{
10 /**
11 * Transform the resource into an array.
12 *
13 * @return array<string, mixed>
14 */
15 public function toArray(Request $request): array
16 {
17 return [
18 'id' => $this->id,
19 'name' => $this->name,
20 'email' => $this->email,
21 'created_at' => $this->created_at,
22 'updated_at' => $this->updated_at,
23 ];
24 }
25}

每个资源类都定义了一个toArray方法,当资源作为路由或控制器方法的响应返回时,该方法返响应转换为 JSON 的属性数组。

请注意,我们可以直接从$this变量访问模型属性。这是因为资源类会自动将属性和方法的访问代理到底层模型,以便于访问。定义资源后,它可以从路由或控制器返回。资源通过其构造函数接受底层模型实例:

1use App\Http\Resources\UserResource;
2use App\Models\User;
3 
4Route::get('/user/{id}', function (string $id) {
5 return new UserResource(User::findOrFail($id));
6});

为了方便起见,您可以使用模型的toResource方法,该方法将使用框架约定自动发现模型的底层资源:

1return User::findOrFail($id)->toResource();

调用该方法时,Laravel 将尝试在最接近模型命名空间的命名空间toResource找到与模型名称匹配的资源,并可选择添加后缀。ResourceHttp\Resources

资源集合

如果要返回资源集合或分页响应,则应collection在路由或控制器中创建资源实例时使用资源类提供的方法:

1use App\Http\Resources\UserResource;
2use App\Models\User;
3 
4Route::get('/users', function () {
5 return UserResource::collection(User::all());
6});

或者,为了方便起见,您可以使用 Eloquent 集合的toResourceCollection方法,它将使用框架约定自动发现模型的底层资源集合:

1return User::all()->toResourceCollection();

调用该方法时,Laravel 将尝试在最接近模型命名空间的命名空间toResourceCollection找到与模型名称匹配且后缀为的资源集合。CollectionHttp\Resources

自定义资源集合

默认情况下,资源集合不允许添加任何可能需要随集合一起返回的自定义元数据。如果您想自定义资源集合的响应,可以创建一个专用资源来表示该集合:

1php artisan make:resource UserCollection

一旦生成了资源集合类,您就可以轻松定义响应中应包含的任何元数据:

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\Json\ResourceCollection;
7 
8class UserCollection extends ResourceCollection
9{
10 /**
11 * Transform the resource collection into an array.
12 *
13 * @return array<int|string, mixed>
14 */
15 public function toArray(Request $request): array
16 {
17 return [
18 'data' => $this->collection,
19 'links' => [
20 'self' => 'link-value',
21 ],
22 ];
23 }
24}

定义资源集合后,它可能从路由或控制器返回:

1use App\Http\Resources\UserCollection;
2use App\Models\User;
3 
4Route::get('/users', function () {
5 return new UserCollection(User::all());
6});

或者,为了方便起见,您可以使用 Eloquent 集合的toResourceCollection方法,它将使用框架约定自动发现模型的底层资源集合:

1return User::all()->toResourceCollection();

调用该方法时,Laravel 将尝试在最接近模型命名空间的命名空间toResourceCollection找到与模型名称匹配且后缀为的资源集合。CollectionHttp\Resources

保存集合键

当从路由返回资源集合时,Laravel 会重置集合的键,使其按数字顺序排列。但是,你可以preserveKeys在资源类中添加一个属性,指示是否保留集合的原始键:

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Resources\Json\JsonResource;
6 
7class UserResource extends JsonResource
8{
9 /**
10 * Indicates if the resource's collection keys should be preserved.
11 *
12 * @var bool
13 */
14 public $preserveKeys = true;
15}

当该preserveKeys属性设置为时true,从路由或控制器返回集合时,集合键将被保留:

1use App\Http\Resources\UserResource;
2use App\Models\User;
3 
4Route::get('/users', function () {
5 return UserResource::collection(User::all()->keyBy->id);
6});

自定义底层资源类

通常,$this->collection资源集合的属性会自动填充将集合中每一项映射到其单个资源类的结果。单个资源类被假定为集合的类名(不包含Collection类名的尾部)。此外,根据您的个人偏好,单个资源类可以添加或不添加 后缀Resource

例如,UserCollection将尝试将给定的用户实例映射到UserResource资源中。要自定义此行为,您可以覆盖$collects资源集合的属性:

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Resources\Json\ResourceCollection;
6 
7class UserCollection extends ResourceCollection
8{
9 /**
10 * The resource that this resource collects.
11 *
12 * @var string
13 */
14 public $collects = Member::class;
15}

写作资源

如果您尚未阅读概念概述,我们强烈建议您在继续阅读本文档之前阅读它。

资源只需要将给定的模型转换为数组。因此,每个资源都包含一个toArray方法,用于将模型的属性转换为 API 友好的数组,该数组可以从应用程序的路由或控制器返回:

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\Json\JsonResource;
7 
8class UserResource extends JsonResource
9{
10 /**
11 * Transform the resource into an array.
12 *
13 * @return array<string, mixed>
14 */
15 public function toArray(Request $request): array
16 {
17 return [
18 'id' => $this->id,
19 'name' => $this->name,
20 'email' => $this->email,
21 'created_at' => $this->created_at,
22 'updated_at' => $this->updated_at,
23 ];
24 }
25}

一旦定义了资源,就可以直接从路由或控制器返回:

1use App\Models\User;
2 
3Route::get('/user/{id}', function (string $id) {
4 return User::findOrFail($id)->toUserResource();
5});

关系

如果您想在响应中包含相关资源,可以将它们添加到资源toArray方法返回的数组中。在此示例中,我们将使用PostResource资源的collection方法将用户的博客文章添加到资源响应中:

1use App\Http\Resources\PostResource;
2use Illuminate\Http\Request;
3 
4/**
5 * Transform the resource into an array.
6 *
7 * @return array<string, mixed>
8 */
9public function toArray(Request $request): array
10{
11 return [
12 'id' => $this->id,
13 'name' => $this->name,
14 'email' => $this->email,
15 'posts' => PostResource::collection($this->posts),
16 'created_at' => $this->created_at,
17 'updated_at' => $this->updated_at,
18 ];
19}

如果您只想在已加载关系时包含关系,请查看有关条件关系的文档。

资源集合

资源会将单个模型转换为数组,而资源集合则会将模型集合转换为数组。不过,没有必要为每个模型定义一个资源集合类,因为所有 Eloquent 模型集合都提供了一种toResourceCollection方法来动态生成一个“临时”的资源集合:

1use App\Models\User;
2 
3Route::get('/users', function () {
4 return User::all()->toResourceCollection();
5});

但是,如果需要自定义随集合返回的元数据,则需要定义自己的资源集合:

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\Json\ResourceCollection;
7 
8class UserCollection extends ResourceCollection
9{
10 /**
11 * Transform the resource collection into an array.
12 *
13 * @return array<string, mixed>
14 */
15 public function toArray(Request $request): array
16 {
17 return [
18 'data' => $this->collection,
19 'links' => [
20 'self' => 'link-value',
21 ],
22 ];
23 }
24}

与单一资源一样,资源集合可以直接从路由或控制器返回:

1use App\Http\Resources\UserCollection;
2use App\Models\User;
3 
4Route::get('/users', function () {
5 return new UserCollection(User::all());
6});

或者,为了方便起见,您可以使用 Eloquent 集合的toResourceCollection方法,它将使用框架约定自动发现模型的底层资源集合:

1return User::all()->toResourceCollection();

调用该方法时,Laravel 将尝试在最接近模型命名空间的命名空间toResourceCollection找到与模型名称匹配且后缀为的资源集合。CollectionHttp\Resources

数据包装

默认情况下,当资源响应转换为 JSON 格式时,最外层资源会被包装在一个data键中。例如,一个典型的资源集合响应如下所示:

1{
2 "data": [
3 {
4 "id": 1,
5 "name": "Eladio Schroeder Sr.",
6 "email": "therese28@example.com"
7 },
8 {
9 "id": 2,
10 "name": "Liliana Mayert",
11 "email": "evandervort@example.com"
12 }
13 ]
14}

如果要禁用最外层资源的包装,则应withoutWrapping在基Illuminate\Http\Resources\Json\JsonResource类上调用该方法。通常,您应该从您的AppServiceProvider或其他服务提供商(在每次向应用程序发出请求时都会加载)调用此方法:

1<?php
2 
3namespace App\Providers;
4 
5use Illuminate\Http\Resources\Json\JsonResource;
6use Illuminate\Support\ServiceProvider;
7 
8class AppServiceProvider extends ServiceProvider
9{
10 /**
11 * Register any application services.
12 */
13 public function register(): void
14 {
15 // ...
16 }
17 
18 /**
19 * Bootstrap any application services.
20 */
21 public function boot(): void
22 {
23 JsonResource::withoutWrapping();
24 }
25}

withoutWrapping方法仅影响最外层的响应,并且不会删除data您手动添加到自己的资源集合中的键。

包装嵌套资源

您可以完全自由地决定如何包装资源的关系。如果您希望所有资源集合都包装在一个data键中,无论它们如何嵌套,都应该为每个资源定义一个资源集合类,并在data键中返回该集合。

您可能想知道这是否会导致最外层的资源被包装在两个data键中。不用担心,Laravel 永远不会让您的资源被意外地双重包装,因此您不必担心正在转换的资源集合的嵌套级别:

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Resources\Json\ResourceCollection;
6 
7class CommentsCollection extends ResourceCollection
8{
9 /**
10 * Transform the resource collection into an array.
11 *
12 * @return array<string, mixed>
13 */
14 public function toArray(Request $request): array
15 {
16 return ['data' => $this->collection];
17 }
18}

数据包装和分页

data当通过资源响应返回分页集合时,即使该withoutWrapping方法已被调用,Laravel 也会将资源数据包装在一个键中。这是因为分页响应始终包含metalinks键,其中包含有关分页器状态的信息:

1{
2 "data": [
3 {
4 "id": 1,
5 "name": "Eladio Schroeder Sr.",
6 "email": "therese28@example.com"
7 },
8 {
9 "id": 2,
10 "name": "Liliana Mayert",
11 "email": "evandervort@example.com"
12 }
13 ],
14 "links":{
15 "first": "http://example.com/users?page=1",
16 "last": "http://example.com/users?page=1",
17 "prev": null,
18 "next": null
19 },
20 "meta":{
21 "current_page": 1,
22 "from": 1,
23 "last_page": 1,
24 "path": "http://example.com/users",
25 "per_page": 15,
26 "to": 10,
27 "total": 10
28 }
29}

分页

您可以将 Laravel 分页器实例传递给collection资源的方法或自定义资源集合:

1use App\Http\Resources\UserCollection;
2use App\Models\User;
3 
4Route::get('/users', function () {
5 return new UserCollection(User::paginate());
6});

或者,为了方便起见,您可以使用分页器的toResourceCollection方法,该方法将使用框架约定自动发现分页模型的底层资源集合:

1return User::paginate()->toResourceCollection();

分页响应始终包含有关分页器状态的信息的键metalinks

1{
2 "data": [
3 {
4 "id": 1,
5 "name": "Eladio Schroeder Sr.",
6 "email": "therese28@example.com"
7 },
8 {
9 "id": 2,
10 "name": "Liliana Mayert",
11 "email": "evandervort@example.com"
12 }
13 ],
14 "links":{
15 "first": "http://example.com/users?page=1",
16 "last": "http://example.com/users?page=1",
17 "prev": null,
18 "next": null
19 },
20 "meta":{
21 "current_page": 1,
22 "from": 1,
23 "last_page": 1,
24 "path": "http://example.com/users",
25 "per_page": 15,
26 "to": 10,
27 "total": 10
28 }
29}

自定义分页信息

如果您想自定义分页响应的links或键中包含的信息,您可以在资源上定义一个方法。此方法将接收数据和信息数组,该数组包含键:metapaginationInformation$paginated$defaultlinksmeta

1/**
2 * Customize the pagination information for the resource.
3 *
4 * @param \Illuminate\Http\Request $request
5 * @param array $paginated
6 * @param array $default
7 * @return array
8 */
9public function paginationInformation($request, $paginated, $default)
10{
11 $default['links']['custom'] = 'https://example.com';
12 
13 return $default;
14}

条件属性

有时,您可能希望仅在满足给定条件时才在资源响应中包含某个属性。例如,您可能希望仅在当前用户为“管理员”时才包含一个值。Laravel 提供了多种辅助方法来帮助您解决这种情况。这些when方法可用于有条件地向资源响应添加属性:

1/**
2 * Transform the resource into an array.
3 *
4 * @return array<string, mixed>
5 */
6public function toArray(Request $request): array
7{
8 return [
9 'id' => $this->id,
10 'name' => $this->name,
11 'email' => $this->email,
12 'secret' => $this->when($request->user()->isAdmin(), 'secret-value'),
13 'created_at' => $this->created_at,
14 'updated_at' => $this->updated_at,
15 ];
16}

在此示例中,仅当已验证用户的方法返回secret时,键才会在最终资源响应中返回。如果方法返回则在将资源响应发送到客户端之前,该键将被从资源响应中删除。该方法允许您在构建数组时,无需使用条件语句,即可清晰地定义资源。isAdmintruefalsesecretwhen

when方法还接受闭包作为其第二个参数,允许您仅当给定条件满足时才计算结果值true

1'secret' => $this->when($request->user()->isAdmin(), function () {
2 return 'secret-value';
3}),

whenHas如果属性确实存在于底层模型中,则可以使用该方法来包含该属性:

1'name' => $this->whenHas('name'),

此外,whenNotNull如果属性不为空,则可以使用此方法在资源响应中包含该属性:

1'name' => $this->whenNotNull($this->name),

合并条件属性

有时,您可能有多个属性仅应基于同一条件包含在资源响应中。在这​​种情况下,您可以使用该mergeWhen方法,仅当给定条件为以下时才在响应中包含这些属性true

1/**
2 * Transform the resource into an array.
3 *
4 * @return array<string, mixed>
5 */
6public function toArray(Request $request): array
7{
8 return [
9 'id' => $this->id,
10 'name' => $this->name,
11 'email' => $this->email,
12 $this->mergeWhen($request->user()->isAdmin(), [
13 'first-secret' => 'value',
14 'second-secret' => 'value',
15 ]),
16 'created_at' => $this->created_at,
17 'updated_at' => $this->updated_at,
18 ];
19}

同样,如果给定的条件是false,则这些属性将在资源响应发送给客户端之前从资源响应中删除。

mergeWhen方法不应在Mix使用字符串和数字键的数组中使用。此外,它也不应在数字键未按顺序排列的数组中使用。

条件关系

除了有条件地加载属性之外,您还可以根据关系是否已在模型中加载,有条件地在资源响应中包含该关系。这允许您的控制器决定哪些关系应该在模型中加载,并且您的资源可以轻松地仅在这些关系实际已加载时才包含它们。最终,这可以更轻松地避免资源中的“N+1”查询问题。

whenLoaded方法可用于有条件地加载关系。为了避免不必要地加载关系,此方法接受关系名称而不是关系本身:

1use App\Http\Resources\PostResource;
2 
3/**
4 * Transform the resource into an array.
5 *
6 * @return array<string, mixed>
7 */
8public function toArray(Request $request): array
9{
10 return [
11 'id' => $this->id,
12 'name' => $this->name,
13 'email' => $this->email,
14 'posts' => PostResource::collection($this->whenLoaded('posts')),
15 'created_at' => $this->created_at,
16 'updated_at' => $this->updated_at,
17 ];
18}

在这个例子中,如果关系尚未加载,posts则在将资源响应发送到客户端之前,将从资源响应中删除该键。

条件关系计数

除了有条件地包含关系之外,您还可以根据关系的计数是否已加载到模型上,有条件地在资源响应中包含关系“计数”:

1new UserResource($user->loadCount('posts'));

whenCounted方法可用于有条件地在资源响应中包含关系计数。如果关系计数不存在,则此方法可避免不必要地包含属性:

1/**
2 * Transform the resource into an array.
3 *
4 * @return array<string, mixed>
5 */
6public function toArray(Request $request): array
7{
8 return [
9 'id' => $this->id,
10 'name' => $this->name,
11 'email' => $this->email,
12 'posts_count' => $this->whenCounted('posts'),
13 'created_at' => $this->created_at,
14 'updated_at' => $this->updated_at,
15 ];
16}

在这个例子中,如果posts关系的计数尚未加载,则posts_count在将资源响应发送到客户端之前,将从资源响应中删除该键。

其他类型的聚合体,例如avg,,,也可以使用下列方法有条件地加载:summinmaxwhenAggregated

1'words_avg' => $this->whenAggregated('posts', 'words', 'avg'),
2'words_sum' => $this->whenAggregated('posts', 'words', 'sum'),
3'words_min' => $this->whenAggregated('posts', 'words', 'min'),
4'words_max' => $this->whenAggregated('posts', 'words', 'max'),

条件枢轴信息

除了在资源响应中有条件地包含关系信息之外,您还可以使用 方法来有条件地包含多对多关系的中间表中的数据whenPivotLoaded。该whenPivotLoaded方法接受数据透视表的名称作为其第一个参数。第二个参数应该是一个闭包,如果数据透视表信息在模型上可用,则返回要返回的值:

1/**
2 * Transform the resource into an array.
3 *
4 * @return array<string, mixed>
5 */
6public function toArray(Request $request): array
7{
8 return [
9 'id' => $this->id,
10 'name' => $this->name,
11 'expires_at' => $this->whenPivotLoaded('role_user', function () {
12 return $this->pivot->expires_at;
13 }),
14 ];
15}

如果您的关系使用自定义中间表模型,则可以将中间表模型的实例作为该whenPivotLoaded方法的第一个参数传递:

1'expires_at' => $this->whenPivotLoaded(new Membership, function () {
2 return $this->pivot->expires_at;
3}),

如果您的中间表使用除 之外的访问器pivot,则可以使用该whenPivotLoadedAs方法:

1/**
2 * Transform the resource into an array.
3 *
4 * @return array<string, mixed>
5 */
6public function toArray(Request $request): array
7{
8 return [
9 'id' => $this->id,
10 'name' => $this->name,
11 'expires_at' => $this->whenPivotLoadedAs('subscription', 'role_user', function () {
12 return $this->subscription->expires_at;
13 }),
14 ];
15}

添加元数据

某些 JSON API 标准要求在资源和资源集合响应中添加元数据。这通常包括links资源或相关资源的信息,或者资源本身的元数据。如果您需要返回有关资源的其他元数据,请将其包含在您的toArray方法中。例如,您可以links在转换资源集合时添加以下信息:

1/**
2 * Transform the resource into an array.
3 *
4 * @return array<string, mixed>
5 */
6public function toArray(Request $request): array
7{
8 return [
9 'data' => $this->collection,
10 'links' => [
11 'self' => 'link-value',
12 ],
13 ];
14}

从资源中返回额外的元数据时,您无需担心意外覆盖Laravel 在返回分页响应时自动添加的links或键。您定义的任何附加内容都将与分页器提供的链接合并。metalinks

顶级元数据

有时,您可能希望仅在资源是返回的最外层资源时,在资源响应中包含某些元数据。通常,这包括整个响应的元信息。要定义此元数据,请with向资源类添加一个方法。此方法应返回一个元数据数组,仅当资源是被转换的最外层资源时,才会将其包含在资源响应中:

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Resources\Json\ResourceCollection;
6 
7class UserCollection extends ResourceCollection
8{
9 /**
10 * Transform the resource collection into an array.
11 *
12 * @return array<string, mixed>
13 */
14 public function toArray(Request $request): array
15 {
16 return parent::toArray($request);
17 }
18 
19 /**
20 * Get additional data that should be returned with the resource array.
21 *
22 * @return array<string, mixed>
23 */
24 public function with(Request $request): array
25 {
26 return [
27 'meta' => [
28 'key' => 'value',
29 ],
30 ];
31 }
32}

构建资源时添加元数据

你也可以在路由或控制器中构建资源实例时添加顶级数据。该additional方法适用于所有资源,它接受一个数组,其中包含应添加到资源响应中的数据:

1return User::all()
2 ->load('roles')
3 ->toResourceCollection()
4 ->additional(['meta' => [
5 'key' => 'value',
6 ]]);

资源响应

正如您已经读到的,资源可以直接从路由和控制器返回:

1use App\Models\User;
2 
3Route::get('/user/{id}', function (string $id) {
4 return User::findOrFail($id)->toResource();
5});

但是,有时您可能需要在将 HTTP 响应发送到客户端之前对其进行自定义。有两种方法可以实现此目的。首先,您可以将该response方法链接到资源上。此方法将返回一个Illuminate\Http\JsonResponse实例,使您可以完全控制响应的标头:

1use App\Http\Resources\UserResource;
2use App\Models\User;
3 
4Route::get('/user', function () {
5 return User::find(1)
6 ->toResource()
7 ->response()
8 ->header('X-Value', 'True');
9});

或者,你可以在资源本身中定义一个withResponse方法。当该资源作为响应中最外层的资源返回时,该方法将被调用:

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\JsonResponse;
6use Illuminate\Http\Request;
7use Illuminate\Http\Resources\Json\JsonResource;
8 
9class UserResource extends JsonResource
10{
11 /**
12 * Transform the resource into an array.
13 *
14 * @return array<string, mixed>
15 */
16 public function toArray(Request $request): array
17 {
18 return [
19 'id' => $this->id,
20 ];
21 }
22 
23 /**
24 * Customize the outgoing response for the resource.
25 */
26 public function withResponse(Request $request, JsonResponse $response): void
27 {
28 $response->header('X-Value', 'True');
29 }
30}