Eloquent: API 资源
介绍
在构建 API 时,您可能需要一个转换层,位于 Eloquent 模型和实际返回给应用程序用户的 JSON 响应之间。Laravel 的资源类允许您以富有表现力和简单的方式将模型和模型集合转换为 JSON。
生成资源
要生成资源类,可以使用 make:resource
Artisan 命令。默认情况下,资源将放置在应用程序的 app/Http/Resources
目录中。资源扩展自 Illuminate\Http\Resources\Json\JsonResource
类:
php artisan make:resource User
资源集合
除了生成转换单个模型的资源外,您还可以生成负责转换模型集合的资源。这允许您的响应包含与给定资源的整个集合相关的链接和其他元信息。
要创建资源集合,您应该在创建资源时使用 --collection
标志。或者,在资源名称中包含 Collection
一词将指示 Laravel 创建集合资源。集合资源扩展自 Illuminate\Http\Resources\Json\ResourceCollection
类:
php artisan make:resource Users --collection
php artisan make:resource UserCollection
概念概述
这是资源和资源集合的高级概述。强烈建议您阅读本文档的其他部分,以深入了解资源提供的自定义和功能。
在深入了解编写资源时可用的所有选项之前,让我们首先从高层次上了解资源在 Laravel 中的使用方式。资源类表示需要转换为 JSON 结构的单个模型。例如,这里是一个简单的 User
资源类:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class User extends JsonResource
{
/**
* 将资源转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
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\User as UserResource;
use App\User;
Route::get('/user', function () {
return new UserResource(User::find(1));
});
资源集合
如果您要返回资源集合或分页响应,可以在路由或控制器中创建资源实例时使用 collection
方法:
use App\Http\Resources\User as UserResource;
use App\User;
Route::get('/user', function () {
return UserResource::collection(User::all());
});
请注意,这不允许添加可能需要与集合一起返回的任何元数据。如果您想自定义资源集合响应,可以创建一个专门的资源来表示集合:
php artisan make:resource UserCollection
一旦生成了资源集合类,您可以轻松定义应该包含在响应中的任何元数据:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* 将资源集合转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'data' => $this->collection,
'links' => [
'self' => 'link-value',
],
];
}
}
在定义资源集合后,可以从路由或控制器返回它:
use App\Http\Resources\UserCollection;
use App\User;
Route::get('/users', function () {
return new UserCollection(User::all());
});
保留集合键
从路由返回资源集合时,Laravel 会重置集合的键,使其按简单的数字顺序排列。但是,您可以在资源类中添加一个 preserveKeys
属性,指示是否应保留集合键:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class User extends JsonResource
{
/**
* 指示是否应保留资源的集合键。
*
* @var bool
*/
public $preserveKeys = true;
}
当 preserveKeys
属性设置为 true
时,集合键将被保留:
use App\Http\Resources\User as UserResource;
use App\User;
Route::get('/user', function () {
return UserResource::collection(User::all()->keyBy->id);
});
自定义底层资源类
通常,资源集合的 $this->collection
属性会自动填充为将集合的每个项目映射到其单个资源类的结果。单个资源类被假定为集合的类名去掉尾随的 Collection
字符串。
例如,UserCollection
将尝试将给定的用户实例映射到 User
资源。要自定义此行为,可以覆盖资源集合的 $collects
属性:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* 此资源收集的资源。
*
* @var string
*/
public $collects = 'App\Http\Resources\Member';
}
编写资源
如果您尚未阅读概念概述,强烈建议您在继续阅读本文档之前先阅读。
本质上,资源很简单。它们只需要将给定的模型转换为数组。因此,每个资源都包含一个 toArray
方法,该方法将模型的属性转换为可以返回给用户的 API 友好数组:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class User extends JsonResource
{
/**
* 将资源转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
一旦定义了资源,就可以直接从路由或控制器返回它:
use App\Http\Resources\User as UserResource;
use App\User;
Route::get('/user', function () {
return new UserResource(User::find(1));
});
关系
如果您想在响应中包含相关资源,可以将它们添加到 toArray
方法返回的数组中。在此示例中,我们将使用 Post
资源的 collection
方法将用户的博客文章添加到资源响应中:
/**
* 将资源转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
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,
];
}
如果您只想在关系已加载时才包含关系,请查看条件关系的文档。
资源集合
虽然资源将单个模型转换为数组,但资源集合将模型集合转换为数组。对于每种模型类型,不必定义资源集合类,因为所有资源都提供了一个 collection
方法,可以即时生成“临时”资源集合:
use App\Http\Resources\User as UserResource;
use App\User;
Route::get('/user', function () {
return UserResource::collection(User::all());
});
但是,如果您需要自定义与集合一起返回的元数据,则需要定义资源集合:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* 将资源集合转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'data' => $this->collection,
'links' => [
'self' => 'link-value',
],
];
}
}
与单个资源一样,资源集合可以直接从路由或控制器返回:
use App\Http\Resources\UserCollection;
use App\User;
Route::get('/users', function () {
return new UserCollection(User::all());
});
数据包装
默认情况下,当资源响应转换为 JSON 时,最外层的资源会被包装在 data
键中。因此,例如,典型的资源集合响应如下所示:
{
"data": [
{
"id": 1,
"name": "Eladio Schroeder Sr.",
"email": "therese28@example.com"
},
{
"id": 2,
"name": "Liliana Mayert",
"email": "evandervort@example.com"
}
]
}
如果您想禁用最外层资源的包装,可以在基础资源类上使用 withoutWrapping
方法。通常,您应该在 AppServiceProvider
或其他在每个请求中加载的服务提供者中调用此方法:
<?php
namespace App\Providers;
use Illuminate\Http\Resources\Json\Resource;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* 注册任何应用程序服务。
*
* @return void
*/
public function register()
{
//
}
/**
* 启动任何应用程序服务。
*
* @return void
*/
public function boot()
{
Resource::withoutWrapping();
}
}
withoutWrapping
方法仅影响最外层的响应,不会删除您手动添加到自己的资源集合中的 data
键。
包装嵌套资源
您可以完全自由地决定如何包装资源的关系。如果您希望所有资源集合都包装在 data
键中,无论其嵌套如何,您应该为每个资源定义一个资源集合类,并在 data
键中返回集合。
您可能会想知道这是否会导致最外层的资源被包装在两个 data
键中。别担心,Laravel 永远不会让您的资源意外地双重包装,因此您不必担心要转换的资源集合的嵌套级别:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class CommentsCollection extends ResourceCollection
{
/**
* 将资源集合转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
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/pagination?page=1",
"last": "http://example.com/pagination?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"path": "http://example.com/pagination",
"per_page": 15,
"to": 10,
"total": 10
}
}
分页
您始终可以将分页器实例传递给资源的 collection
方法或自定义资源集合:
use App\Http\Resources\UserCollection;
use App\User;
Route::get('/users', function () {
return new UserCollection(User::paginate());
});
分页响应始终包含有关分页器状态的 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/pagination?page=1",
"last": "http://example.com/pagination?page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"path": "http://example.com/pagination",
"per_page": 15,
"to": 10,
"total": 10
}
}
条件属性
有时,您可能希望仅在满足给定条件时才在资源响应中包含属性。例如,您可能希望仅在当前用户是“管理员”时才包含一个值。Laravel 提供了多种辅助方法来帮助您解决这种情况。when
方法可用于有条件地将属性添加到资源响应中:
/**
* 将资源转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'secret' => $this->when(Auth::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(Auth::user()->isAdmin(), function () {
return 'secret-value';
}),
合并条件属性
有时,您可能有几个属性应该仅在给定条件为 true
时才包含在资源响应中。在这种情况下,您可以使用 mergeWhen
方法仅在给定条件为 true
时将属性包含在响应中:
/**
* 将资源转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
$this->mergeWhen(Auth::user()->isAdmin(), [
'first-secret' => 'value',
'second-secret' => 'value',
]),
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
同样,如果给定条件为 false
,这些属性将在发送给客户端之前从资源响应中完全删除。
mergeWhen
方法不应在混合字符串和数字键的数组中使用。此外,它不应在未按顺序排列的数字键数组中使用。
条件关系
除了有条件地加载属性外,您还可以根据关系是否已加载到模型上来有条件地在资源响应中包含关系。这允许您的控制器决定应加载哪些关系到模型上,并且您的资源可以轻松地仅在它们实际加载时才包含它们。
最终,这使得在资源中更容易避免“N+1”查询问题。whenLoaded
方法可用于有条件地加载关系。为了避免不必要地加载关系,此方法接受关系的名称而不是关系本身:
/**
* 将资源转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
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
键将在发送给客户端之前从资源响应中完全删除。
条件枢纽信息
除了在资源响应中有条件地包含关系信息外,您还可以使用 whenPivotLoaded
方法有条件地包含多对多关系的中间表中的数据。whenPivotLoaded
方法接受枢纽表的名称作为其第一个参数。第二个参数应为一个闭包,定义如果枢纽信息可用在模型上时要返回的值:
/**
* 将资源转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'expires_at' => $this->whenPivotLoaded('role_user', function () {
return $this->pivot->expires_at;
}),
];
}
如果您的中间表使用的访问器不是 pivot
,可以使用 whenPivotLoadedAs
方法:
/**
* 将资源转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'expires_at' => $this->whenPivotLoadedAs('subscription', 'role_user', function () {
return $this->subscription->expires_at;
}),
];
}
添加元数据
一些 JSON API 标准要求在资源和资源集合响应中添加元数据。这通常包括诸如资源或相关资源的 links
,或关于资源本身的元数据。如果您需要返回有关资源的其他元数据,请将其包含在 toArray
方法中。例如,您可以在转换资源集合时包含 link
信息:
/**
* 将资源转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'data' => $this->collection,
'links' => [
'self' => 'link-value',
],
];
}
在从资源返回其他元数据时,您永远不必担心意外覆盖分页器在返回分页响应时自动添加的 links
或 meta
键。您定义的任何其他 links
都将与分页器提供的链接合并。
顶级元数据
有时,您可能希望仅在资源是返回的最外层资源时才包含某些元数据。通常,这包括有关响应整体的元信息。要定义此元数据,请在资源类中添加一个 with
方法。此方法应返回一个元数据数组,仅在资源是被渲染的最外层资源时才包含在资源响应中:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* 将资源集合转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return parent::toArray($request);
}
/**
* 获取应与资源数组一起返回的其他数据。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function with($request)
{
return [
'meta' => [
'key' => 'value',
],
];
}
}
在构建资源时添加元数据
您还可以在路由或控制器中构建资源实例时添加顶级数据。所有资源都可以使用 additional
方法,该方法接受一个应添加到资源响应中的数据数组:
return (new UserCollection(User::all()->load('roles')))
->additional(['meta' => [
'key' => 'value',
]]);
资源响应
如您所读,资源可以直接从路由和控制器返回:
use App\Http\Resources\User as UserResource;
use App\User;
Route::get('/user', function () {
return new UserResource(User::find(1));
});
但是,有时您可能需要在发送给客户端之前自定义传出的 HTTP 响应。有两种方法可以实现此目的。首先,您可以在资源上链接 response
方法。此方法将返回一个 Illuminate\Http\JsonResponse
实例,允许您完全控制响应的头:
use App\Http\Resources\User as UserResource;
use App\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\Resources\Json\JsonResource;
class User extends JsonResource
{
/**
* 将资源转换为数组。
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
];
}
/**
* 自定义资源的传出响应。
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Response $response
* @return void
*/
public function withResponse($request, $response)
{
$response->header('X-Value', 'True');
}
}