Laravel 的 JSON:API
Resource
这是一个轻量级的 Laravel API 资源包,可帮助你遵循 JSON:API
标准,内置支持稀疏字段集、复合文档等功能。
注意 这些文档并非旨在介绍
JSON:API
规范及相关概念,如果你还不熟悉,应该前往阅读规范。以下文档仅涵盖如何通过该包来实现规范。
目录
版本支持
- PHP:
8.1
,8.2
,8.3
- Laravel:
9.0
,10.0
,11.0
安装
你可以使用 composer 从 Packagist 安装。
composer require timacdonald/json-api
入门
该包提供的 JsonApiResource
类是 Laravel Eloquent API 资源的特殊化。所有公开的 API 仍然可以访问;例如,在控制器中,你可以像使用 Laravel 的 JsonResource
类一样使用 JsonApiResource
。
<?php
namespace App\Http\Controllers;
use App\Http\Resources\UserResource;
use App\Models\User;
class UserController
{
public function index()
{
$users = User::with([/* ... */])->paginate();
return UserResource::collection($users);
}
public function show(User $user)
{
$user->load([/* ... */]);
return UserResource::make($user);
}
}
随着我们深入示例,你会看到在内部与该类交互时引入了新的 API,例如,不再使用 toArray()
方法。
创建你的第一个 JSON:API
资源
让我们为 User
模型创建一个 UserResource
。在用户资源中,我们将在响应中公开用户的 name
、website
和 twitter_handle
。
首先我们创建一个继承 TiMacDonald\JsonApi\JsonApiResource
的新 API 资源。
<?php
namespace App\Http\Resources;
use TiMacDonald\JsonApi\JsonApiResource;
class UserResource extends JsonApiResource
{
//
}
添加属性
现在我们将创建一个 $attributes
属性,并列出我们想要公开的模型属性。
<?php
namespace App\Http\Resources;
use TiMacDonald\JsonApi\JsonApiResource;
class UserResource extends JsonApiResource
{
public $attributes = [
'name',
'website',
'twitter_handle',
];
}
当向返回 UserResource
的端点发出请求时,例如:
Route::get('users/{user}', fn (User $user) => UserResource::make($user));
将返回以下 JSON:API
格式的数据:
{
"data": {
"type": "users",
"id": "74812",
"attributes": {
"name": "Tim",
"website": "https://timacdonald.me",
"twitter_handle": "@timacdonald87"
}
}
}
🎉 你刚刚创建了你的第一个 JSON:API
资源 🎉
恭喜...真是太刺激了!
现在我们将深入添加资源的关系,但如果你想探索更复杂的属性功能,可以跳到前面:
添加关系
可用的关系可以在 $relationships
属性中指定,类似于 $attributes
属性,但你可以使用键/值对来提供给定关系应使用的资源类。
我们将在资源上提供两个关系:
$user->team
: 一个 "toOne" /HasOne
关系。$user->posts
: 一个 "toMany" /HasMany
关系。
<?php
namespace App\Http\Resources;
use TiMacDonald\JsonApi\JsonApiResource;
class UserResource extends JsonApiResource
{
public $attributes = [
'name',
'website',
'twitter_handle',
];
public $relationships = [
'team' => TeamResource::class,
'posts' => PostResource::class,
];
}
假设键/值对遵循 '{myKey}' => {MyKey}Resource::class
的约定,可以省略类以进一步简化。
<?php
namespace App\Http\Resources;
use TiMacDonald\JsonApi\JsonApiResource;
class UserResource extends JsonApiResource
{
public $attributes = [
'name',
'website',
'twitter_handle',
];
public $relationships = [
'team',
'posts',
];
}
示例请求和响应
客户端现在可以通过 include
查询参数请求这些关系。
GET /users/74812?include=posts,team
注意 除非调用客户端通过
include
查询参数请求,否则不会在响应中暴露关系。这是有意的,是JSON:API
规范的一部分。
{
"data": {
"id": "74812",
"type": "users",
"attributes": {
"name": "Tim",
"website": "https://timacdonald.me",
"twitter_handle": "@timacdonald87"
},
"relationships": {
"posts": {
"data": [
{
"type": "posts",
"id": "25240"
},
{
"type": "posts",
"id": "39974"
}
]
},
"team": {
"data": {
"type": "teams",
"id": "18986"
}
}
}
},
"included": [
{
"id": "25240",
"type": "posts",
"attributes": {
"title": "So what is `JSON:API` all about anyway?",
"content": "...",
"excerpt": "..."
}
},
{
"id": "39974",
"type": "posts",
"attributes": {
"title": "Building an API with Laravel, using the `JSON:API` specification.",
"content": "...",
"excerpt": "..."
}
},
{
"id": "18986",
"type": "teams",
"attributes": {
"name": "Laravel"
}
}
]
}
要了解更复杂的关系特性,你可能想跳到前面:
关于预加载的说明
该包不会预加载 Eloquent 关系。如果关系未预加载,该包将即时延迟加载关系。我强烈建议使用 Spatie 的查询构建器包,它将根据 JSON:API
查询参数标准预加载你的模型。
Spatie 提供了关于如何使用该包的全面文档,但我会简要举例说明如何在控制器中使用它。
<?php
namespace App\Http\Controllers;
use App\Http\Resources\UserResource;
use App\Models\User;
use Spatie\QueryBuilder\QueryBuilder;
class UserController
{
public function index()
{
$users = QueryBuilder::for(User::class)
->allowedIncludes(['team', 'posts'])
->paginate();
return UserResource::collection($users);
}
public function show($id)
{
$user = QueryBuilder::for(User::class)
->allowedIncludes(['team', 'posts'])
->findOrFail($id);
return UserResource::make($user);
}
}
深入了解
我们现在已经介绍了在资源上公开属性和关系的基础知识。我们现在将介绍更高级的主题,以便让你获得更大的控制权。
属性
toAttributes()
正如我们在添加属性部分看到的,$attributes
属性是公开资源属性的最快方法。在某些情况下,你可能需要对公开的属性进行更大的控制。如果是这种情况,你可以实现 toAttributes()
方法。这将让你可以访问当前请求并允许条件逻辑。
<?php
namespace App\Http\Resources;
use TiMacDonald\JsonApi\JsonApiResource;
class UserResource extends JsonApiResource
{
/**
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
*/
public function toAttributes($request)
{
return [
'name' => $this->name,
'website' => $this->website,
'twitter_handle' => $this->twitter_handle,
'email' => $this->when($this->email_is_public, $this->email, '<private>'),
'address' => [
'city' => $this->address('city'),
'country' => $this->address('country'),
],
];
}
}
示例响应
{
"data": {
"id": "74812",
"type": "users",
"attributes": {
"name": "Tim",
"website": "https://timacdonald.me",
"twitter_handle": "@timacdonald87",
"email": "<private>",
"address": {
"city": "Melbourne",
"country": "Australia"
}
}
}
}
稀疏字段集
稀疏字段集是 JSON:API
规范的一个特性,允许客户端指定他们想要接收的任何给定资源类型的属性。这允许更确定性的响应,同时也提高了服务器端性能并减小了有效载荷大小。稀疏字段集可以开箱即用于你的资源。
我们将在这里简要介绍它们,但我们建议阅读规范以了解更多信息。
例如,假设我们正在构建博客的索引页面。该页面将显示每篇文章的 title
和 excerpt
,以及文章作者的 name
。如果客户端愿意,他们可以将响应限制为仅包含每种资源类型所需的属性,并排除其他属性,例如文章的 content
和作者的 twitter_handle
。
为了实现这一点,我们将发送以下请求。
GET /posts?include=author&fields[posts]=title,excerpt&fields[users]=name
注意
include
查询参数键是author
,而稀疏字段集参数键是users
。这是因为作者就是用户,例如 Eloquentauthor()
关系返回User
模型。
示例响应
{
"data": [
{
"id": "25240",
"type": "posts",
"attributes": {
"title": "So what is `JSON:API` all about anyway?",
"excerpt": "..."
},
"relationships": {
"author": {
"data": {
"type": "users",
"id": "74812"
}
}
}
},
{
"id": "39974",
"type": "posts",
"attributes": {
"title": "Building an API with Laravel, using the `JSON:API` specification.",
"excerpt": "..."
},
"relationships": {
"author": {
"data": {
"type": "users",
"id": "74812"
}
}
}
}
],
"included": [
{
"type": "users",
"id": "74812",
"attributes": {
"name": "Tim"
}
}
]
}
最小属性
当不使用稀疏字段集时,资源会返回最大的属性有效载荷,即返回资源上声明的所有属性。如果你愿意,你可以要求使用稀疏字段集才能检索任何属性。
你可以在应用程序服务提供者中调用 useMinimalAttributes()
方法。
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use TiMacDonald\JsonApi\JsonApiResource;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
JsonApiResource::useMinimalAttributes();
对于计算成本较高的属性,可以设置为仅在需要包含在响应中时才进行评估,即它们未通过稀疏字段集或最小属性被排除。如果你在资源中与数据库交互或发送HTTP请求,这可能会很有用。
举个例子,假设我们为每个用户提供一个base64编码的头像。我们的实现是从内部的头像微服务下载头像。
```php
<?php
namespace App\Http\Resources;
use Illuminate\Support\Facades\Http;
use TiMacDonald\JsonApi\JsonApiResource;
class UserResource extends JsonApiResource
{
/**
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
*/
public function toAttributes($request)
{
return [
// ...
'avatar' => Http::get("https://avatar.example.com/{$this->id}")->body(),
];
}
}
上面的实现即使在客户端通过稀疏字段集或最小属性排除 avatar
属性时,也会向我们的微服务发送HTTP请求。为了在不返回此属性时提高性能,我们可以将值包装在一个 Closure
中。只有在需要返回 avatar
时,才会评估这个 Closure
。
<?php
namespace App\Http\Resources;
use Illuminate\Support\Facades\Http;
use TiMacDonald\JsonApi\JsonApiResource;
class UserResource extends JsonApiResource
{
/**
* @param \Illuminate\Http\Request $request
* @return array<string, mixed>
*/
public function toAttributes($request)
{
return [
// ...
'avatar' => fn () => Http::get("https://avatar.example.com/{$this->id}")->body(),
];
}
}
关系
toRelationships()
正如我们在添加关系部分看到的,$relationships
属性是为资源指定可用关系的最快方式。在某些情况下,你可能需要对提供的关系进行更多控制。如果是这种情况,你可以实现 toRelationships()
方法。这将允许你访问当前请求并进行条件逻辑。
值必须始终包装在一个 Closure
中,只有在客户端请求关系时才会调用。
<?php
namespace App\Http\Resources;
use TiMacDonald\JsonApi\JsonApiResource;
class UserResource extends JsonApiResource
{
/**
* @param \Illuminate\Http\Request $request
* @return array<string, (callable(): \TiMacDonald\JsonApi\JsonApiResource|\TiMacDonald\JsonApi\JsonApiResourceCollection|\Illuminate\Http\Resources\PotentiallyMissing)>
*/
public function toRelationships($request)
{
return [
'team' => fn () => TeamResource::make($this->team),
'posts' => fn () => $request->user()->is($this->resource)
? PostResource::collection($this->posts)
: PostResource::collection($this->posts->where('published', true)),
];
}
}
自定义关系资源类猜测