Appearance
Eloquent API 资源
介绍
在构建 API 时,您可能需要一个转换层,该层位于 Eloquent 模型和实际返回给应用程序用户的 JSON 响应之间。例如,您可能希望为用户子集显示某些属性,而不是其他用户,或者您可能希望始终在模型的 JSON 表示中包含某些关系。Eloquent 的资源类允许您以富有表现力的方式轻松地将模型和模型集合转换为 JSON。
当然,您始终可以使用他们的 toJson
方法将 Eloquent 模型或集合转换为 JSON;但是,Eloquent 资源对模型的 JSON 序列化及其关系提供了更精细、更强大的控制。
生成资源
要生成资源类,您可以使用 make:resource
Artisan 命令。默认情况下,资源将放置在应用程序的 app/Http/Resources
目录中。Resources 扩展类 Illuminate\Http\Resources\Json\JsonResource
:
shell
php artisan make:resource UserResource
资源集合
除了生成转换单个模型的资源外,还可以生成负责转换模型集合的资源。这允许您的 JSON 响应包含与给定资源的整个集合相关的链接和其他元信息。
要创建资源集合,您应该在创建资源时使用 --collection
标志。或者,在资源名称中包含单词 Collection
将向 Laravel 表明它应该创建一个集合资源。集合资源扩展了 Illuminate\Http\Resources\Json\ResourceCollection
类:
shell
php artisan make:resource User --collection
php artisan make:resource UserCollection
概念概述
NOTE
这是资源和资源集合的高级概述。强烈建议您阅读本文档的其他部分,以更深入地了解资源为您提供的自定义和功能。
在深入研究编写资源时可用的所有选项之前,让我们先简要了解一下 Laravel 中如何使用资源。资源类表示需要转换为 JSON 结构的单个模型。例如,下面是一个简单的 UserResource
资源类:
php
<?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
变量访问模型属性。这是因为资源类会自动将属性和方法访问代理到底层模型,以便于访问。定义资源后,可以从 route 或 controller 返回它。资源通过其构造函数接受底层模型实例:
php
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/user/{id}', function (string $id) {
return new UserResource(User::findOrFail($id));
});
资源集合
如果要返回资源集合或分页响应,则应在路由或控制器中创建资源实例时使用资源类提供的 collection
方法:
php
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/users', function () {
return UserResource::collection(User::all());
});
请注意,这不允许添加任何可能需要随集合一起返回的自定义元数据。如果您想自定义资源集合响应,您可以创建一个专用资源来表示集合:
shell
php artisan make:resource UserCollection
生成资源集合类后,您可以轻松定义响应中应包含的任何元数据:
php
<?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',
],
];
}
}
定义资源集合后,它可能从路由或控制器返回:
php
use App\Http\Resources\UserCollection;
use App\Models\User;
Route::get('/users', function () {
return new UserCollection(User::all());
});
保留集合键
当从路由返回资源集合时,Laravel 会重置集合的键,以便它们按数字顺序排列。但是,您可以将 preserveKeys
属性添加到资源类中,以指示是否应保留集合的原始键:
php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* 指示是否应保留资源的集合键。
*
* @var bool
*/
public $preserveKeys = true;
}
当 preserveKeys
属性设置为 true
时,当从路由或控制器返回集合时,将保留集合键:
php
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
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* 此资源收集的资源
*
* @var string
*/
public $collects = Member::class;
}
写作资源
NOTE
如果您尚未阅读概念概述,强烈建议您在继续阅读本文档之前先阅读。
资源只需要将给定模型转换为数组。因此,每个资源都包含一个 toArray
方法,该方法将模型的属性转换为可从应用程序的路由或控制器返回的 API 友好数组:
php
<?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,
];
}
}
定义资源后,可以直接从 route 或 controller 返回它:
php
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
方法将用户的博客文章添加到资源响应中:
php
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,
];
}
NOTE
如果您只想在已加载关系时包含关系,请查看有关条件关系的文档。
资源集合
资源将单个模型转换为数组,而资源集合将模型集合转换为数组。但是,并非绝对必要为每个模型定义一个资源集合类,因为所有资源都提供了一个集合
方法来动态生成“临时”资源集合:
php
use App\Http\Resources\UserResource;
use App\Models\User;
Route::get('/users', function () {
return UserResource::collection(User::all());
});
但是,如果您需要自定义随集合返回的元数据,则需要定义自己的资源集合:
php
<?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',
],
];
}
}
与单个资源一样,资源集合可以直接从 routes 或 controller 返回:
php
use App\Http\Resources\UserCollection;
use App\Models\User;
Route::get('/users', function () {
return new UserCollection(User::all());
});
数据包装
默认情况下,当资源响应转换为 JSON 时,您的最外层资源将包装在数据
键中。因此,例如,典型的资源集合响应如下所示:
json
{
"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
<?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();
}
}
WARNING
withoutWrapping
方法仅影响最外层的响应,不会删除您手动添加到自己的资源集合中的数据
键。
包装嵌套资源
您可以完全自由地决定如何包装资源的关系。如果您希望所有资源集合都包装在一个数据
键中,而不管它们的嵌套如何,您应该为每个资源定义一个资源集合类,并在数据
键中返回该集合。
您可能想知道这是否会导致最外层的资源被包装在两个数据
键中。别担心,Laravel 永远不会让你的资源被意外地双重包装,所以你不必担心你正在转换的资源集合的嵌套级别:
php
<?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 也会将您的资源数据包装在数据
键中。这是因为分页响应始终包含 meta
并将 keys 与分页器状态信息链接
在一起:
json
{
"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
键,其中包含有关 paginator 状态的信息:
json
{
"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
}
}
自定义分页信息
如果要自定义分页响应的链接或``元
键中包含的信息,则可以在资源上定义 paginationInformation
方法。该方法将接收 $paginated
数据和 $default
信息的数组,该数组是一个包含链接
和元
键的数组:
php
/**
* 自定义资源的分页信息
*
* @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;
}
条件属性
有时,您可能希望仅在满足给定条件时在资源响应中包含属性。例如,您可能希望仅在当前用户是 “administrator” 时才包含一个值。Laravel 提供了多种辅助方法来帮助你处理这种情况。when
方法可用于有条件地向资源响应添加属性:
php
/**
* 将资源转换为数组。
*
* @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
,则只有在最终资源响应中才会返回密钥
。如果该方法返回 false
,则在将密钥发送到客户端之前,将从资源响应中删除密钥
。when
方法允许您在构建数组时表达性地定义资源,而无需求助于条件语句。
when
方法还接受一个闭包作为其第二个参数,允许你仅在给定条件为 true
时计算结果值:
php
'secret' => $this->when($request->user()->isAdmin(), function () {
return 'secret-value';
}),
如果 whenHas
实际存在于底层模型上,则可以使用 whenHas 方法来包含属性:
php
'name' => $this->whenHas('name'),
此外,如果属性不为 null,则 whenNotNull
方法可用于在资源响应中包含属性:
php
'name' => $this->whenNotNull($this->name),
合并条件属性
有时,您可能有多个属性,这些属性只应包含在基于相同条件的资源响应中。在这种情况下,仅当给定条件为 true
时,才能使用 mergeWhen
方法在响应中包含属性:
php
/**
* 将资源转换为数组
*
* @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
方法可用于有条件地加载关系。为了避免不必要地加载关系,此方法接受关系的名称,而不是关系本身:
php
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
键将在发送到客户端之前从资源响应中删除。
条件关系计数
除了有条件地包含关系之外,您还可以根据关系的计数是否已加载到模型上,有条件地在资源响应中包含关系“计数”:
php
new UserResource($user->loadCount('posts'));
whenCounted
方法可用于在资源响应中有条件地包含关系的计数。如果关系的计数不存在,此方法可避免不必要地包含该属性:
php
/**
* 将资源转换为数组
*
* @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
方法有条件地加载:
php
'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
方法接受数据透视表的名称作为其第一个参数。第二个参数应该是一个闭包,如果 pivot 信息在模型上可用,则返回要返回的值:
php
/**
* 将资源转换为数组。
*
* @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
方法:
php
'expires_at' => $this->whenPivotLoaded(new Membership, function () {
return $this->pivot->expires_at;
}),
如果中间表使用非 pivot
的访问器,则可以使用 whenPivotLoadedAs
方法:
php
/**
* 将资源转换为数组。
*
* @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 标准要求将元数据添加到您的资源和资源集合响应中。这通常包括指向资源或相关资源的链接
,或有关资源本身的元数据等内容。如果需要返回有关资源的其他元数据,请将其包含在 toArray
方法中。例如,在转换资源集合时,您可以包含链接
信息:
php
/**
* 将资源转换为数组
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'links' => [
'self' => 'link-value',
],
];
}
当您从资源返回其他元数据时,您永远不必担心在返回分页响应时意外覆盖 Laravel 自动添加的链接
或元
键。您定义的任何其他链接
都将与 paginator 提供的链接合并。
顶级元数据
有时,如果资源是返回的最外层资源,则您可能希望仅在资源响应中包含某些元数据。通常,这包括有关整个响应的元信息。要定义此元数据,请将 with
方法添加到您的资源类中。仅当资源是要转换的最外层资源时,此方法才应返回要包含在资源响应中的元数据数组:
php
<?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',
],
];
}
}
在构建资源时添加元数据
在路由或控制器中构造资源实例时,你也可以添加顶级数据。附加
方法(在所有资源上都可用)接受应添加到资源响应的数据数组:
php
在构建资源时添加元数据 return (new UserCollection(User::all()->load('roles')))
->additional(['meta' => [
'key' => 'value',
]]);
资源响应
正如您已经阅读的,资源可以直接从 routes 和 controller 返回:
php
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
实例,让您完全控制响应的标头:
php
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
<?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');
}
}