API 资源
简介
在构建 API 时,你可能需要一个位于 Eloquent 模型和实际返回给应用用户的 JSON 响应之间的转换层。例如,你可能希望为部分用户显示某些属性而不向其他用户显示,或者你可能希望始终在模型的 JSON 表示中包含某些关系。Eloquent 的资源类允许你以表达性强且简单的方式将模型和模型集合转换为 JSON。
当然,你可以随时使用 Eloquent 模型或集合的 toJson 方法将它们转换为 JSON;然而,Eloquent 资源提供了对模型及其关系的 JSON 序列化更细粒度和更健壮的控制。
典型业务场景:
- 用户列表 API 脱敏:通过
UserResource只暴露id、name、email,隐藏手机号和内部标记,管理员请求时再通过when条件追加敏感字段 - 订单详情嵌套关系:在
OrderResource中嵌套OrderItemResource::collection,一次请求返回订单及其全部商品明细,避免前端多次请求 - 分页元数据标准化:使用资源集合返回分页数据,自动携带
meta(总条数、当前页)和links(上下页 URL),前端无需额外解析 - JSON:API 规范对接:使用
JsonApiResource生成符合 JSON:API 标准的响应结构,支持客户端通过include和fields参数按需加载关系和字段 - 条件性关联输出:通过
whenLoaded只在控制器预加载了关系时才输出关联数据,避免 N+1 查询问题 - 自定义响应头注入:通过
withResponse方法在资源响应中注入X-Rate-Limit等自定义 HTTP 头,满足 API 网关需求
生成资源
要生成资源类,你可以使用 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
概念概述
信息
这是资源和资源集合的高级概述。强烈建议你阅读本文档的其他部分,以更深入地了解资源提供的自定义能力和强大功能。
在深入了解编写资源时可用的所有选项之前,让我们先从高层次了解资源在 Laravel 中的使用方式。资源类代表需要转换为 JSON 结构的单个模型。例如,这里是一个简单的 UserResource 资源类:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* 将资源转换为数组。
*
* @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));
});
为方便起见,你可以使用模型的 toResource 方法,它将使用框架约定自动发现模型的底层资源:
return User::findOrFail($id)->toResource();
调用 toResource 方法时,Laravel 将尝试在最接近模型命名空间的 Http\Resources 命名空间中定位与模型名称匹配且可选地以 Resource 为后缀的资源。
如果你的资源类不遵循此命名约定或位于不同的命名空间中,你可以使用 UseResource 属性指定模型的默认资源:
<?php
namespace App\Models;
use App\Http\Resources\CustomUserResource;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\UseResource;
#[UseResource(CustomUserResource::class)]
class User extends Model
{
// ...
}
或者,你可以通过将资源类传递给 toResource 方法来指定:
return User::findOrFail($id)->toResource(CustomUserResource::class);
资源集合
如果你要返回资源集合或分页响应,你应该在路由或控制器中创建资源实例时使用资源类提供的 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();
调用 toResourceCollection 方法时,Laravel 将尝试在最接近模型命名空间的 Http\Resources 命名空间中定位与模型名称匹配且以 Collection 为后缀的资源集合。
如果你的资源集合类不遵循此命名约定或位于不同的命名空间中,你可以使用 UseResourceCollection 属性指定模型的默认资源集合:
<?php
namespace App\Models;
use App\Http\Resources\CustomUserCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\UseResourceCollection;
#[UseResourceCollection(CustomUserCollection::class)]
class User extends Model
{
// ...
}
或者,你可以通过将资源集合类传递给 toResourceCollection 方法来指定:
return User::all()->toResourceCollection(CustomUserCollection::class);
自定义资源集合
默认情况下,资源集合不允许添加可能需要随集合返回的自定义元数据。如果你想自定义资源集合响应,你可以创建一个专用资源来表示集合:
php artisan make:resource UserCollection
生成资源集合类后,你可以轻松定义应包含在响应中的任何元数据:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* 将资源集合转换为数组。
*
* @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());
});
或者,为方便起见,你可以使用 Eloquent 集合的 toResourceCollection 方法,它将使用框架约定自动发现模型的底层资源集合:
return User::all()->toResourceCollection();
调用 toResourceCollection 方法时,Laravel 将尝试在最接近模型命名空间的 Http\Resources 命名空间中定位与模型名称匹配且以 Collection 为后缀的资源集合。
保留集合键
当从路由返回资源集合时,Laravel 会重置集合的键使其按数字顺序排列。然而,你可以在资源类上使用 PreserveKeys 属性来指示是否应保留集合的原始键:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Attributes\PreserveKeys;
use Illuminate\Http\Resources\Json\JsonResource;
#[PreserveKeys]
class UserResource extends JsonResource
{
// ...
}
当 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\Attributes\Collects;
use Illuminate\Http\Resources\Json\ResourceCollection;
#[Collects(Member::class)]
class UserCollection extends ResourceCollection
{
// ...
}
编写资源
信息
如果你还没有阅读概念概述,强烈建议你在继续阅读本文档之前先阅读它。
资源只需要将给定模型转换为数组。因此,每个资源都包含一个 toArray 方法,将模型的属性转换为可以从应用的路由或控制器返回的 API 友好数组:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* 将资源转换为数组。
*
* @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\Models\User;
Route::get('/user/{id}', function (string $id) {
return User::findOrFail($id)->toUserResource();
});
关系
如果你想在响应中包含关联资源,你可以将它们添加到资源的 toArray 方法返回的数组中。在此示例中,我们将使用 PostResource 资源的 collection 方法将用户的博客文章添加到资源响应中:
use App\Http\Resources\PostResource;
use Illuminate\Http\Request;
/**
* 将资源转换为数组。
*
* @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,
];
}
信息
如果你只想在关系已经加载时才包含它们,请查看条件关系文档。
资源集合
资源将单个模型转换为数组,而资源集合将模型集合转换为数组。然而,并非绝对需要为每个模型定义一个资源集合类,因为所有 Eloquent 模型集合都提供了 toResourceCollection 方法来即时生成「临时」资源集合:
use App\Models\User;
Route::get('/users', function () {
return User::all()->toResourceCollection();
});
然而,如果你需要自定义随集合返回的元数据,则有必要定义你自己的资源集合:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* 将资源集合转换为数组。
*
* @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());
});
或者,为方便起见,你可以使用 Eloquent 集合的 toResourceCollection 方法,它将使用框架约定自动发现模型的底层资源集合:
return User::all()->toResourceCollection();
调用 toResourceCollection 方法时,Laravel 将尝试在最接近模型命名空间的 Http\Resources 命名空间中定位与模型名称匹配且以 Collection 为后缀的资源集合。
数据包裹
默认情况下,当资源响应转换为 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
{
/**
* 注册任何应用服务。
*/
public function register(): void
{
// ...
}
/**
* 引导任何应用服务。
*/
public function boot(): void
{
JsonResource::withoutWrapping();
}
}
注意
withoutWrapping 方法只影响最外层的响应,不会移除你自己手动添加到资源集合中的 data 键。
包裹嵌套资源
你可以完全自由地决定资源的关系如何被包裹。如果你希望所有资源集合都包裹在 data 键中,无论它们的嵌套层级如何,你应该为每个资源定义一个资源集合类,并在 data 键中返回集合。
你可能会想知道这是否会导致你最外层的资源被包裹在两个 data 键中。不用担心,Laravel 永远不会让你的资源被意外地双重包裹,所以你不必担心你正在转换的资源集合的嵌套层级:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CommentsCollection extends ResourceCollection
{
/**
* 将资源集合转换为数组。
*
* @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());
});
或者,为方便起见,你可以使用分页器的 toResourceCollection 方法,它将使用框架约定自动发现分页模型的底层资源集合:
return User::paginate()->toResourceCollection();
分页响应始终包含带有分页器状态信息的 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 数据和 $default 信息数组,该数组包含 links 和 meta 键:
/**
* 自定义资源的分页信息。
*
* @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 方法可用于有条件地将属性添加到资源响应中:
/**
* 将资源转换为数组。
*
* @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,
];
}
在此示例中,secret 键只会在经过身份验证的用户的 isAdmin 方法返回 true 时才在最终资源响应中返回。如果该方法返回 false,secret 键将在资源响应发送到客户端之前被移除。when 方法允许你在构建数组时无需诉诸条件语句即可表达性地定义资源。
when 方法还接受闭包作为其第二个参数,允许你只在给定条件为 true 时才计算结果值:
'secret' => $this->when($request->user()->isAdmin(), function () {
return 'secret-value';
}),
whenHas 方法可用于在属性确实存在于底层模型上时才包含它:
'name' => $this->whenHas('name'),
此外,whenNotNull 方法可用于在属性不为 null 时在资源响应中包含它:
'name' => $this->whenNotNull($this->name),
合并条件属性
有时你可能有多个属性只应基于相同条件才包含在资源响应中。在这种情况下,你可以使用 mergeWhen 方法仅在给定条件为 true 时才将属性包含在响应中:
/**
* 将资源转换为数组。
*
* @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,这些属性将在资源响应发送到客户端之前被移除。
注意
mergeWhen 方法不应在混合使用字符串键和数字键的数组中使用。此外,它不应在数字键不是按顺序排列的数组中使用。
条件关系
除了有选择地加载属性外,你还可以根据关系是否已在模型上加载来有选择地在资源响应中包含关系。这允许你的控制器决定应在模型上加载哪些关系,而你的资源可以轻松地仅在实际加载了关系时才包含它们。最终,这使得在资源中避免「N+1」查询问题变得更容易。
whenLoaded 方法可用于有条件地加载关系。为了避免不必要地加载关系,此方法接受关系的名称而不是关系本身:
use App\Http\Resources\PostResource;
/**
* 将资源转换为数组。
*
* @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 方法可用于有条件地在资源响应中包含关系的计数。如果关系的计数不存在,此方法会避免不必要地包含该属性:
/**
* 将资源转换为数组。
*
* @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 方法接受中间表名称作为其第一个参数。第二个参数应该是一个闭包,返回当中间表信息在模型上可用时应返回的值:
/**
* 将资源转换为数组。
*
* @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 方法:
/**
* 将资源转换为数组。
*
* @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 信息:
/**
* 将资源转换为数组。
*
* @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
{
/**
* 将资源集合转换为数组。
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return parent::toArray($request);
}
/**
* 获取应随资源数组返回的额外数据。
*
* @return array<string, mixed>
*/
public function with(Request $request): array
{
return [
'meta' => [
'key' => 'value',
],
];
}
}
构造资源时添加元数据
你还可以在路由或控制器中构造资源实例时添加顶层数据。所有资源上可用的 additional 方法接受应添加到资源响应中的数据数组:
return User::all()
->load('roles')
->toResourceCollection()
->additional(['meta' => [
'key' => 'value',
]]);
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 命令:
php artisan make:resource PostResource --json-api
生成的类将继承 Illuminate\Http\Resources\JsonApi\JsonApiResource,并包含供你定义的 $attributes 和 $relationships 属性:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
class PostResource extends JsonApiResource
{
/**
* 资源的属性。
*/
public $attributes = [
// ...
];
/**
* 资源的关系。
*/
public $relationships = [
// ...
];
}
JSON:API 资源可以像标准资源一样从路由和控制器返回:
use App\Http\Resources\PostResource;
use App\Models\Post;
Route::get('/api/posts/{post}', function (Post $post) {
return new PostResource($post);
});
或者,为方便起见,你可以使用模型的 toResource 方法:
Route::get('/api/posts/{post}', function (Post $post) {
return $post->toResource();
});
这将生成符合 JSON:API 规范的响应:
{
"data": {
"id": "1",
"type": "posts",
"attributes": {
"title": "Hello World",
"body": "This is my first post."
}
}
}
要返回 JSON:API 资源集合,请使用 collection 方法或 toResourceCollection 便捷方法:
return PostResource::collection(Post::all());
return Post::all()->toResourceCollection();
定义属性
有两种方法可以定义 JSON:API 资源中包含哪些属性。
最简单的方法是在资源上定义 $attributes 属性。你可以将属性名称列为值,这些值将直接从底层模型读取:
public $attributes = [
'title',
'body',
'created_at',
];
如果某个属性的计算成本很高,你可以从 toAttributes 以闭包形式返回它,这样它只会在响应中实际需要该属性时才被求值。
或者,要完全控制资源的属性,你可以覆盖资源上的 toAttributes 方法:
/**
* 获取资源的属性。
*
* @return array<string, mixed>
*/
public function toAttributes(Request $request): array
{
return [
'title' => $this->title,
'body' => $this->body,
'is_published' => fn () => $this->published_at !== null,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
定义关系
JSON:API 资源支持定义遵循 JSON:API 规范的关系。关系仅在客户端通过 include 查询参数请求时才被序列化。
$relationships 属性
你可以通过资源上的 $relationships 属性定义资源的可包含关系:
public $relationships = [
'author',
'comments',
];
当将关系名称列为值时,Laravel 将解析相应的 Eloquent 关系并自动发现适当的资源类。如果你需要显式指定资源类,可以将关系定义为键 / 类对:
use App\Http\Resources\UserResource;
public $relationships = [
'author' => UserResource::class,
'comments',
];
或者,你可以覆盖资源上的 toRelationships 方法:
/**
* 获取资源的关系。
*/
public function toRelationships(Request $request): array
{
return [
'author' => UserResource::class,
'comments' => fn () => CommentResource::collection(
$request->user()->is($this->resource)
? $this->comments
: $this->comments->where('is_public', true),
),
];
}
使用闭包可以让你对关系有效载荷拥有更多控制权,同时仍然只在客户端请求时才解析关系。
包含关系
客户端可以使用 include 查询参数请求关联资源:
GET /api/posts/1?include=author,comments
这将生成一个响应,其中 relationships 键中包含资源标识符对象,顶层 included 数组中包含完整的资源对象:
{
"data": {
"id": "1",
"type": "posts",
"attributes": {
"title": "Hello World"
},
"relationships": {
"author": {
"data": {
"id": "1",
"type": "users"
}
},
"comments": {
"data": [
{
"id": "1",
"type": "comments"
}
]
}
}
},
"included": [
{
"id": "1",
"type": "users",
"attributes": {
"name": "Taylor Otwell"
}
},
{
"id": "1",
"type": "comments",
"attributes": {
"body": "Great post!"
}
}
]
}
可以使用点号表示法包含嵌套关系:
GET /api/posts/1?include=comments.author
关系深度
默认情况下,嵌套关系包含被限制在最大深度内。你可以使用 maxRelationshipDepth 方法自定义此限制,通常在应用的某个服务提供者中设置:
use Illuminate\Http\Resources\JsonApi\JsonApiResource;
JsonApiResource::maxRelationshipDepth(3);
资源类型与 ID
默认情况下,资源的 type 派生自资源类名。例如,PostResource 生成类型 posts,BlogPostResource 生成 blog-posts。资源的 id 从模型的主键解析。
如果你需要自定义这些值,你可以覆盖资源上的 toType 和 toId 方法:
/**
* 获取资源的类型。
*/
public function toType(Request $request): string
{
return 'articles';
}
/**
* 获取资源的 ID。
*/
public function toId(Request $request): string
{
return (string) $this->uuid;
}
当资源的类型应与其类名不同时,这特别有用,例如当 AuthorResource 包裹 User 模型并应输出类型 authors 时。
稀疏字段集与包含
JSON:API 资源支持稀疏字段集,允许客户端使用 fields 查询参数为每种资源类型仅请求特定属性:
GET /api/posts?fields[posts]=title,created_at&fields[users]=name
这将只包含 posts 资源的 title 和 created_at 属性,以及 users 资源的 name 属性。
忽略查询字符串
如果你想禁用给定资源响应的稀疏字段集过滤,你可以调用 ignoreFieldsAndIncludesInQueryString 方法:
return $post->toResource()
->ignoreFieldsAndIncludesInQueryString();
包含先前已加载的关系
默认情况下,关系仅在通过 include 查询参数请求时才包含在响应中。如果你想无论查询字符串如何都包含所有先前预加载的关系,你可以调用 includePreviouslyLoadedRelationships 方法:
return $post->load('author', 'comments')
->toResource()
->includePreviouslyLoadedRelationships();
链接与元信息
你可以通过覆盖资源上的 toLinks 和 toMeta 方法向 JSON:API 资源对象添加链接和元信息:
/**
* 获取资源的链接。
*/
public function toLinks(Request $request): array
{
return [
'self' => route('api.posts.show', $this->resource),
];
}
/**
* 获取资源的元信息。
*/
public function toMeta(Request $request): array
{
return [
'readable_created_at' => $this->created_at->diffForHumans(),
];
}
这将在响应中的资源对象上添加 links 和 meta 键:
{
"data": {
"id": "1",
"type": "posts",
"attributes": {
"title": "Hello World"
},
"links": {
"self": "https://example.com/api/posts/1"
},
"meta": {
"readable_created_at": "2 hours ago"
}
}
}
资源响应
如你已经阅读到的,资源可以直接从路由和控制器返回:
use App\Models\User;
Route::get('/user/{id}', function (string $id) {
return User::findOrFail($id)->toResource();
});
然而,有时你可能需要在将 HTTP 响应发送到客户端之前对其进行自定义。有两种方法可以实现。首先,你可以在资源上链式调用 response 方法。此方法将返回一个 Illuminate\Http\JsonResponse 实例,让你完全控制响应的头:
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/user', function () {
return User::find(1)
->toResource()
->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
{
/**
* 将资源转换为数组。
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
];
}
/**
* 自定义资源的传出响应。
*/
public function withResponse(Request $request, JsonResponse $response): void
{
$response->header('X-Value', 'True');
}
}