当构建 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[!NOTE]
这是资源和资源集合的高级概述。强烈建议您阅读本文档的其他部分,以深入了解资源为您提供的定制化能力和强大功能。
在深入了解编写资源时可用的所有选项之前,让我们首先从宏观层面了解资源在 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));
});为了方便,您可以使用模型的 toResource 方法,它将使用框架约定来自动发现模型的底层资源:
return User::findOrFail($id)->toResource();当调用 toResource 方法时,Laravel 将尝试查找一个与模型名称匹配并可选地以 Resource 为后缀且位于最接近模型命名空间的 Http\Resources 命名空间中的资源。
如果您的资源类不遵循此命名约定或位于不同的命名空间中,您可以使用 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());
});`
return User::all()->toResourceCollection();当调用 toResourceCollection 方法时,Laravel 将尝试定位一个资源集合,该集合的名称与模型名称匹配,并以 Collection 为后缀,位于最接近模型命名空间的 Http\Resources 命名空间内。
如果你的资源集合类不遵循此命名约定或位于不同的命名空间中,你可以使用 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
{
/**
* 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());
});或者,为了方便,您可以使用 Eloquent 集合的 toResourceCollection 方法,它将使用框架约定来自动发现模型的底层资源集合:
return User::all()->toResourceCollection();当调用 toResourceCollection 方法时,Laravel 将尝试定位一个资源集合,该集合与模型名称匹配,并以 Collection 为后缀,位于 Http\Resources 命名空间内,且该命名空间最接近模型所在的命名空间。
从路由返回资源集合时,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;
}[!注意]
如果您尚未阅读概念概述,强烈建议您在继续阅读本文档之前先阅读。
资源只需将给定的模型转换为数组。 因此,每个资源都包含一个 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\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;
/**
* 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,
];
}[!注意]
如果您只想在关系已加载时才包含它们,请查阅关于条件关系的文档。
虽然资源将单个模型转换成一个数组,但资源集合将模型集合转换成一个数组。然而,并非绝对必要为你的每个模型定义一个资源集合类,因为所有 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
{
/**
* 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());
});或者,为方便起见,你可以使用 Eloquent 集合的 toResourceCollection 方法,它将利用框架约定自动发现模型的基础资源集合:
return User::all()->toResourceCollection();当调用 toResourceCollection 方法时,Laravel 将尝试在最接近模型命名空间的 Http\Resources 命名空间内,查找一个名称与模型名称匹配且以 Collection 为后缀的资源集合。
**By default**, your outermost resource is wrapped in adata` key when the resource response is converted to JSON. So, for example, a typical resource collection response looks like the following:
{
"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
{
/**
* Register any application services.
*/
public function register(): void
{
// ...
}
/**
* Bootstrap any application services.
*/
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
{
/**
* Transform the resource collection into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return ['data' => $this->collection];
}
}当通过资源响应返回分页集合时,Laravel 会将你的资源数据包裹在一个 data 键中即使已经调用了 withoutWrapping 方法。这是因为分页响应总是包含 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 键:
/**
* 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'),此外,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,这些属性将在发送给客户端之前从资源响应中移除。
[!WARNING]
该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')); ` when, an optional dependency is supplied it will be resolved; otherwise, the parent container will be resolved.**
/**
* 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 键将从资源响应中移除,然后才发送给客户端。
其他类型的聚合函数,例如 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 方法接受枢纽表的名称作为它的第一个参数。第二个参数应该是一个闭包,如果模型上存在枢纽信息,该闭包将返回相应的值:
/**
* 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 标准要求向您的资源和资源集合响应中添加元数据。这通常包括诸如 链接 到资源或相关资源的信息,或关于资源本身的元数据。如果您需要返回关于资源的额外元数据,请将其包含在您的 toArray 方法中。例如,您可能在转换资源集合时包含 链接 信息:
/**
* 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 在返回分页响应时自动添加的 links 或 meta 键. 您定义的任何额外 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 User::all()
->load('roles')
->toResourceCollection()
->additional(['meta' => [
'key' => 'value',
]]);正如你已经读到的,资源可以直接从路由和控制器中返回:
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
{
/**
* 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');
}
}