跳转到内容

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

概念概述

lightbulb

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

在深入研究编写资源时可用的所有选项之前,让我们首先大致了解一下如何在 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;
}

编写资源

lightbulb

如果你尚未阅读概念概述,强烈建议你在继续阅读本文档之前这样做。

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

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

资源集合

虽然资源将单个模型转换为数组,但资源集合将模型集合转换为数组。但是,没有绝对必要为每个模型定义资源集合类,因为所有资源都提供了一个 collection 方法来动态生成“临时”资源集合

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": "[email protected]"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "[email protected]"
}
]
}

如果您想禁用最外层资源的包装,您应该在基础 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();
}
}
exclamation

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 键中。这是因为分页响应始终包含带有分页器状态信息的 metalinks 键。

{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "[email protected]"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "[email protected]"
}
],
"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());
});

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

{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "[email protected]"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "[email protected]"
}
],
"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
}
}

自定义分页信息

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

/**
* 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,则在发送到客户端之前,这些属性将从资源响应中删除。

exclamation

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 键。

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

'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 在返回分页响应时自动添加的 linksmeta 键。您定义的任何额外 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');
}
}