跳转至内容

Eloquent: API 资源

简介

在构建 API 时,你可能需要一个转换层,位于 Eloquent 模型与实际返回给用户应用的 JSON 响应之间。例如,你可能希望向部分用户显示特定属性而不向其他用户显示,或者希望在模型的 JSON 表示中始终包含某些关联。Eloquent 的资源类允许你以富有表现力且简便的方式将模型和模型集合转换为 JSON。

当然,你总是可以使用 Eloquent 模型或集合的 toJson 方法将其转换为 JSON;然而,Eloquent 资源对模型的 JSON 序列化及其关联提供了更细粒度且健壮的控制。

生成资源

要生成资源类,可以使用 make:resource Artisan 命令。默认情况下,资源会被放置在应用程序的 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();

调用 toResource 方法时,Laravel 将尝试在最接近模型命名空间的 Http\Resources 命名空间中定位与模型名称匹配且可选后缀为 Resource 的资源。

如果你的资源类不遵循此命名约定,或者位于不同的命名空间中,你可以使用 UseResource 属性为模型指定默认资源。

1<?php
2 
3namespace App\Models;
4 
5use App\Http\Resources\CustomUserResource;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Database\Eloquent\Attributes\UseResource;
8 
9#[UseResource(CustomUserResource::class)]
10class User extends Model
11{
12 // ...
13}

或者,你可以通过将资源类传递给 toResource 方法来指定资源类。

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

资源集合

如果你返回的是资源集合或分页响应,则应在路由或控制器中创建资源实例时,使用资源类提供的 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();

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

如果你的资源集合类不遵循此命名约定,或者位于不同的命名空间中,你可以使用 UseResourceCollection 属性为模型指定默认资源集合。

1<?php
2 
3namespace App\Models;
4 
5use App\Http\Resources\CustomUserCollection;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Database\Eloquent\Attributes\UseResourceCollection;
8 
9#[UseResourceCollection(CustomUserCollection::class)]
10class User extends Model
11{
12 // ...
13}

或者,你可以通过将资源集合类传递给 toResourceCollection 方法来指定资源集合类。

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

自定义资源集合

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

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();

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

保留集合键

当从路由返回资源集合时,Laravel 会重置集合的键,使其按数字顺序排列。但是,你可以在资源类上使用 PreserveKeys 属性,以指明是否应保留集合的原始键。

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Resources\Attributes\PreserveKeys;
6use Illuminate\Http\Resources\Json\JsonResource;
7 
8#[PreserveKeys]
9class UserResource extends JsonResource
10{
11 // ...
12}

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\Attributes\Collects;
6use Illuminate\Http\Resources\Json\ResourceCollection;
7 
8#[Collects(Member::class)]
9class UserCollection extends ResourceCollection
10{
11 // ...
12}

编写资源

如果你还没有阅读概念概述,强烈建议在继续阅读本文档之前先阅读它。

资源只需要将给定的模型转换为数组。因此,每个资源都包含一个 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();

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

数据包装

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

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

如果你想禁用最外层资源的包装,则应在基础的 Illuminate\Http\Resources\Json\JsonResource 类上调用 withoutWrapping 方法。通常,你应该在 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}

数据包装与分页

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

1{
2 "data": [
3 {
4 "id": 1,
5 "name": "Eladio Schroeder Sr.",
6 "email": "[email protected]"
7 },
8 {
9 "id": 2,
10 "name": "Liliana Mayert",
11 "email": "[email protected]"
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": "[email protected]"
7 },
8 {
9 "id": 2,
10 "name": "Liliana Mayert",
11 "email": "[email protected]"
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}

自定义分页信息

如果你想自定义分页响应的 linksmeta 键中包含的信息,可以在资源上定义一个 paginationInformation 方法。该方法将接收 $paginated 数据和包含 linksmeta 键的 $default 信息数组。

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}

在此示例中,只有当已验证用户的 isAdmin 方法返回 true 时,secret 键才会在最终的资源响应中返回。如果该方法返回 false,则 secret 键将在发送到客户端之前从资源响应中移除。when 方法允许你富有表现力地定义资源,而无需在构建数组时使用条件语句。

when 方法的第二个参数也可以接受闭包,允许你仅在给定条件为 true 时才计算结果值。

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

whenHas 方法可用于在属性实际存在于底层模型上时将其包含进来。

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

此外,whenNotNull 方法可用于在属性不为 null 时将其包含在资源响应中。

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 方法不应在混合字符串和数字键的数组中使用。此外,它不应在包含非顺序排序的数字键的数组中使用。

条件关联

除了有条件地加载属性外,你还可以根据关联是否已在模型上加载,有条件地在资源响应中包含关联。这允许你的控制器决定模型上应该加载哪些关联,而你的资源可以轻松地仅在它们实际加载时包含它们。最终,这使得在资源中避免“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 键将在发送到客户端之前从资源响应中移除。

其他类型的聚合,例如 avgsumminmax,也可以使用 whenAggregated 方法有条件地加载。

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'),

条件中间表(Pivot)信息

除了在资源响应中有条件地包含关联信息外,你还可以使用 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 在返回分页响应时自动添加的 linksmeta 键。你定义的任何额外 links 都将与分页器提供的链接合并。

顶级元数据

有时,如果资源是返回的最外层资源,你可能希望仅在资源响应中包含特定的元数据。通常,这包括关于整个响应的元信息。要定义此元数据,请向资源类添加 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 ]]);

JSON:API 资源

Laravel 自带 JsonApiResource,这是一种生成符合 JSON:API 规范的响应的资源类。它继承自标准的 JsonResource 类,并自动处理资源对象结构、关联、稀疏字段集、包含项,并将 Content-Type 头设置为 application/vnd.api+json

Laravel 的 JSON:API 资源处理响应的序列化。如果你还需要解析传入的 JSON:API 查询参数(如过滤和排序),Spatie 的 Laravel Query Builder 是一个极好的辅助扩展包。

生成 JSON:API 资源

要生成 JSON:API 资源,请使用带有 --json-api 标志的 make:resource Artisan 命令。

1php artisan make:resource PostResource --json-api

生成的类将继承 Illuminate\Http\Resources\JsonApi\JsonApiResource,并包含 $attributes$relationships 属性供你定义。

1<?php
2 
3namespace App\Http\Resources;
4 
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\JsonApi\JsonApiResource;
7 
8class PostResource extends JsonApiResource
9{
10 /**
11 * The resource's attributes.
12 */
13 public $attributes = [
14 // ...
15 ];
16 
17 /**
18 * The resource's relationships.
19 */
20 public $relationships = [
21 // ...
22 ];
23}

JSON:API 资源可以像标准资源一样从路由和控制器中返回。

1use App\Http\Resources\PostResource;
2use App\Models\Post;
3 
4Route::get('/api/posts/{post}', function (Post $post) {
5 return new PostResource($post);
6});

或者,为了方便起见,你可以使用模型的 toResource 方法。

1Route::get('/api/posts/{post}', function (Post $post) {
2 return $post->toResource();
3});

这将生成一个符合 JSON:API 的响应。

1{
2 "data": {
3 "id": "1",
4 "type": "posts",
5 "attributes": {
6 "title": "Hello World",
7 "body": "This is my first post."
8 }
9 }
10}

要返回 JSON:API 资源集合,请使用 collection 方法或 toResourceCollection 辅助方法。

1return PostResource::collection(Post::all());
2 
3return Post::all()->toResourceCollection();

定义属性

有两种方法可以定义 JSON:API 资源中包含哪些属性。

最简单的方法是在资源上定义 $attributes 属性。你可以列出属性名称作为值,这些值将直接从底层模型中读取。

1public $attributes = [
2 'title',
3 'body',
4 'created_at',
5];

或者,为了完全控制资源的属性,你可以重写资源上的 toAttributes 方法。

1/**
2 * Get the resource's attributes.
3 *
4 * @return array<string, mixed>
5 */
6public function toAttributes(Request $request): array
7{
8 return [
9 'title' => $this->title,
10 'body' => $this->body,
11 'is_published' => $this->published_at !== null,
12 'created_at' => $this->created_at,
13 'updated_at' => $this->updated_at,
14 ];
15}

定义关联

JSON:API 资源支持定义遵循 JSON:API 规范的关联。关联仅在客户端通过 include 查询参数请求时才会被序列化。

$relationships 属性

你可以通过资源上的 $relationships 属性定义资源的可包含关联。

1public $relationships = [
2 'author',
3 'comments',
4];

当将关联名称列为值时,Laravel 将解析相应的 Eloquent 关联并自动发现合适的资源类。如果你需要显式指定资源类,可以将关联定义为键/类对。

1use App\Http\Resources\UserResource;
2 
3public $relationships = [
4 'author' => UserResource::class,
5 'comments',
6];

或者,你可以重写资源上的 toRelationships 方法。

1/**
2 * Get the resource's relationships.
3 */
4public function toRelationships(Request $request): array
5{
6 return [
7 'author' => UserResource::class,
8 'comments',
9 ];
10}

包含关联

客户端可以使用 include 查询参数请求相关资源。

1GET /api/posts/1?include=author,comments

这将生成一个响应,其中 relationships 键中包含资源标识符对象,顶级 included 数组中包含完整的资源对象。

1{
2 "data": {
3 "id": "1",
4 "type": "posts",
5 "attributes": {
6 "title": "Hello World"
7 },
8 "relationships": {
9 "author": {
10 "data": {
11 "id": "1",
12 "type": "users"
13 }
14 },
15 "comments": {
16 "data": [
17 {
18 "id": "1",
19 "type": "comments"
20 }
21 ]
22 }
23 }
24 },
25 "included": [
26 {
27 "id": "1",
28 "type": "users",
29 "attributes": {
30 "name": "Taylor Otwell"
31 }
32 },
33 {
34 "id": "1",
35 "type": "comments",
36 "attributes": {
37 "body": "Great post!"
38 }
39 }
40 ]
41}

嵌套关联可以使用点符号包含。

1GET /api/posts/1?include=comments.author

关联深度

默认情况下,嵌套关联包含受限于最大深度。你可以在应用程序的服务提供者之一中使用 maxRelationshipDepth 方法自定义此限制。

1use Illuminate\Http\Resources\JsonApi\JsonApiResource;
2 
3JsonApiResource::maxRelationshipDepth(3);

资源类型与 ID

默认情况下,资源的 type 派生自资源类名。例如,PostResource 生成类型 postsBlogPostResource 生成 blog-posts。资源的 id 从模型的主键中解析。

如果你需要自定义这些值,可以重写资源上的 toTypetoId 方法。

1/**
2 * Get the resource's type.
3 */
4public function toType(Request $request): string
5{
6 return 'articles';
7}
8 
9/**
10 * Get the resource's ID.
11 */
12public function toId(Request $request): string
13{
14 return (string) $this->uuid;
15}

这在资源类型应与其类名不同时特别有用,例如当 AuthorResource 包装 User 模型且应输出类型 authors 时。

稀疏字段集与包含项

JSON:API 资源支持 稀疏字段集,允许客户端使用 fields 查询参数仅请求每种资源类型的特定属性。

1GET /api/posts?fields[posts]=title,created_at&fields[users]=name

这将仅包含 posts 资源的 titlecreated_at 属性,以及 users 资源的 name 属性。

忽略查询字符串

如果你想为给定的资源响应禁用稀疏字段集过滤,可以调用 ignoreFieldsAndIncludesInQueryString 方法。

1return $post->toResource()
2 ->ignoreFieldsAndIncludesInQueryString();

包含预先加载的关联

默认情况下,关联仅在通过 include 查询参数请求时才包含在响应中。如果你想包含所有之前已预加载的关联,而不考虑查询字符串,可以调用 includePreviouslyLoadedRelationships 方法。

1return $post->load('author', 'comments')
2 ->toResource()
3 ->includePreviouslyLoadedRelationships();

你可以通过重写资源上的 toLinkstoMeta 方法,将链接和元信息添加到你的 JSON:API 资源对象中。

1/**
2 * Get the resource's links.
3 */
4public function toLinks(Request $request): array
5{
6 return [
7 'self' => route('api.posts.show', $this->resource),
8 ];
9}
10 
11/**
12 * Get the resource's meta information.
13 */
14public function toMeta(Request $request): array
15{
16 return [
17 'readable_created_at' => $this->created_at->diffForHumans(),
18 ];
19}

这将在响应的资源对象中添加 linksmeta 键。

1{
2 "data": {
3 "id": "1",
4 "type": "posts",
5 "attributes": {
6 "title": "Hello World"
7 },
8 "links": {
9 "self": "https://example.com/api/posts/1"
10 },
11 "meta": {
12 "readable_created_at": "2 hours ago"
13 }
14 }
15}

资源响应

如你所读,资源可以直接从路由和控制器中返回。

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}