galviadigital / json-api
适用于 Laravel 的轻量级 JSON:API 资源
Requires
- php: ^8.1 || ^8.2
- illuminate/collections: ^9.0 || ^10.0
- illuminate/database: ^9.0 || ^10.0
- illuminate/http: ^9.0 || ^10.0
- illuminate/support: ^9.0 || ^10.0
- symfony/http-kernel: ^6.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.13
- laravel/framework: ^9.0 || ^10.0
- opis/json-schema: ^2.3
- orchestra/testbench: ^7.0 || ^8.0
- phpunit/phpunit: ^9.0
README
JSON:API
资源用于 Laravel
一个轻量级的 Laravel API 资源,帮助您遵循 JSON:API
标准,支持稀疏字段集、复合文档等。这是对 Timacdonald/json=api 的修改版本,用于修复一些尚未合并到主分支的未解决的问题。
注意 这些文档不是为了向您介绍
JSON:API
规范和相关概念而设计的,如果您还不熟悉它,应该阅读规范。下面的文档仅涵盖通过此包实现规范的方法。
目录
版本支持
- PHP:
8.1
,8.2
- Laravel:
^8.73.2
,^9.0
,10.x-dev
安装
您可以通过 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
。
首先,我们将创建一个新的 API 资源,该资源扩展了 TiMacDonald\JsonApi\JsonApiResource
。
<?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 { /** * @var string[] */ 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
:一个 "一对一" /HasOne
关系。$user->posts
:一个 "一对多" /HasMany
关系。
<?php namespace App\Http\Resources; use TiMacDonald\JsonApi\JsonApiResource; class UserResource extends JsonApiResource { /** * @var string[] */ public $attributes = [ 'name', 'website', 'twitter_handle', ]; /** * @var array<string, class-string<JsonApiResource>> */ 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 { /** * @var string[] */ public $attributes = [ 'name', 'website', 'twitter_handle', ]; /** * @var string[] */ 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
,以及作者的名字。如果客户端希望,他们可以限制响应,仅包含每个资源类型的所需属性,并排除其他属性,例如文章的 content
和作者的 twitter_handle
。
为了实现这一点,我们将发送以下请求。
GET /posts?include=author&fields[posts]=title,excerpt&fields[users]=name
注意
include
查询参数的键是author
,而稀疏字段集参数的键是users
。这是因为作者 是 用户,例如 Eloquent 的author()
关系返回一个User
模型。
示例响应
{ "data": [ { "id": "25240", "type": "posts", "attributes": { "title": "So what is `JSON:API` all about anyway?", "excerpt": "..." }, "relationships": { "author": { "data": { "type": "users", "id": "74812", "meta": {} }, "meta": {}, "links": {} } }, "meta": {}, "links": {} }, { "id": "39974", "type": "posts", "attributes": { "title": "Building an API with Laravel, using the `JSON:API` specification.", "excerpt": "..." }, "relationships": { "author": { "data": { "type": "users", "id": "74812", "meta": {} }, "meta": {}, "links": {} } }, "meta": {}, "links": {} } ], "included": [ { "type": "users", "id": "74812", "attributes": { "name": "Tim" }, "relationships": {}, "meta": {}, "links": {} } ] }
最小属性
当不使用 稀疏字段集 时,资源返回最大属性有效载荷,即返回资源上声明的所有属性。如果您愿意,可以强制使用稀疏字段集来检索 任何 属性。
您可以在应用程序服务提供者中调用 minimalAttributes()
方法。
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use TiMacDonald\JsonApi\JsonApiResource; class AppServiceProvider extends ServiceProvider { public function boot() { JsonApiResource::minimalAttributes(); // ... } }
懒属性评估
对于计算成本较高的属性,它们只会在它们要包含在响应中时进行评估,即它们没有被通过 稀疏字段集 或 最小属性 排除。如果在与数据库交互或在资源中进行 HTTP 请求时,这可能很有用。
例如,让我们假设我们为每个用户公开一个 base64 编码的头像。我们的实现将从我们内部的头像微服务中下载头像。
<?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)), ]; } }
自定义关系资源类猜测
//----- 以下内容为工作进度,请忽略 ------- //
资源标识
[JSON:API
文档:标识](https://jsonapi.fullstack.org.cn/format/#document-resource-object-identification)
我们已经为您定义了合理的默认值,这样您就可以在不需要调整琐碎细节的情况下立即开始工作。
如果您仅使用 Eloquent 模型使用资源,则系统会自动为您解决资源 "id"
和 "type"
。
"id"
通过调用 $model->getKey()
方法解决,而 "type"
则通过使用模型的表名驼峰命名法解决,例如 blog_posts
变为 blogPosts
。
您可以自定义此操作以支持其他类型的对象和行为,但这将在 高级用法 部分中介绍。
不错。那很简单,让我们继续...
资源链接
[JSON:API
文档:链接](https://jsonapi.fullstack.org.cn/format/#document-resource-object-links)
要为资源提供链接,您可以实现 toLinks($request)
方法...
<?php use TiMacDonald\JsonApi\Link; class UserResource extends JsonApiResource { public function toLinks($request): array { return [ Link::self(route('users.show', $this->resource)), 'related' => 'https://example.com/related' ]; } }
资源元数据
[JSON:API
文档:元数据](https://jsonapi.fullstack.org.cn/format/#document-meta)
要为资源提供元数据,您可以实现 toMeta($request)
方法...
<?php class UserResource extends JsonApiResource { public function toMeta($request): array { return [ 'resourceDeprecated' => true, ]; } }
高级用法
资源标识
自定义资源 "id"
您可以通过在服务提供者中指定 ID 解决器来自定义 id
的解析。
<?php class AppServiceProvider extends ServiceProvider { public function boot() { JsonApiResource::resolveIdUsing(function (mixed $resource, Request $request): string { // your custom resolution logic... }); } }
虽然不建议,但您也可以根据资源逐个覆盖 toId(Request $request): string
方法。
自定义资源 "type"
您可以通过在服务提供者中指定类型解决器来自定义 type
的解析。
<?php class AppServiceProvider extends ServiceProvider { public function boot() { JsonApiResource::resolveTypeUsing(function (mixed $resource, Request $request): string { // your custom resolution logic... }); } }
虽然不建议,但您也可以根据资源逐个覆盖 toType(Request $request): string
方法。
资源关系
[JSON:API
文档:相关资源的包含](https://jsonapi.fullstack.org.cn/format/#fetching-includes)
关系可以深度解析,也可以包含多个关系路径。当然,您应该注意 n+1 问题,这就是为什么我们建议与 Spatie 的查询构建器 一起使用此包的原因。
# Including deeply nested relationships /api/posts/8?include=author.comments # Including multiple relationship paths /api/posts/8?include=comments,author.comments
- 使用 "whenLoaded" 是一种反模式
致谢
- Tim MacDonald
- Jess Archer 对于共同创建我们的初始内部版本和头脑风暴
- 所有贡献者
特别感谢 Caneco 为我们的标志提供的帮助 ✨
v1 todo
- 服务器实现重新思考。
- 重新思考对象和属性的命名
- 所有内容使用驼峰命名法
- 允许资源指定其JsonResource类。
- 将所有缓存设置为WeakMaps。
- 必须在构造函数中首先处理的事项。请参阅链接:href