timacdonald / json-api
Laravel 的轻量级 JSON:API 资源
Requires
- php: ~8.1.0 || ~8.2.0 || ~8.3.0
- illuminate/collections: ^9.0 || ^10.0 || ^11.0
- illuminate/database: ^9.0 || ^10.0 || ^11.0
- illuminate/http: ^9.0 || ^10.0 || ^11.0
- illuminate/support: ^9.0 || ^10.0 || ^11.0
- symfony/http-kernel: ^6.0 || ^7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.13
- laravel/framework: ^9.0 || ^10.0 || ^11.0
- opis/json-schema: ^2.3
- orchestra/testbench: ^7.0 || ^8.0 || ^9.0
- phpunit/phpunit: ^9.0 || ^10.5
This package is auto-updated.
Last update: 2024-09-19 19:22:17 UTC
README
JSON:API
资源用于 Laravel
这是一个轻量级的 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
。
首先,我们将创建一个新的 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 { 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 { 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
。这是因为作者 是 用户,例如,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" } } } }, { "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 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)), ]; } }
自定义关系资源类猜测
//----- 以下内容均为WIP,请忽略 ------- //
资源标识
[JSON:API
文档:标识](https://jsonapi.org/format/#document-resource-object-identification)
我们为您定义了一个合理的默认值,这样您就可以立即开始运行而无需调整细节。
如果您仅使用 Eloquent 模型中的资源,则资源 "id"
和 "type"
会自动为您解决。
"id"
通过调用 $model->getKey()
方法解决,而 "type"
通过使用模型的表名驼峰命名法解决,例如 blog_posts
变为 blogPosts
。
您可以自定义此功能以支持其他类型的对象和行为,但这将在 高级用法 部分中介绍。
太好了,这很简单,那么让我们继续...
资源链接
[JSON:API
文档:链接](https://jsonapi.org/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.org/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
方法来自定义 id
。
自定义资源 "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
方法来自定义 type
。
资源关系
[JSON:API
文档:相关资源的包含](https://jsonapi.org/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)为标志✨所做的贡献 Caneco
v1 待办事项
- 服务器实现重新思考。
- 重新思考对象和属性的命名。
- 所有内容都使用驼峰命名法。
- 允许资源指定它们的JsonResource类。
- 将所有缓存改为WeakMaps。
- 某些“必须”在构造函数中首先执行的事情。请参阅链接:href
- 是否应该使用withResourceIdentifier或mapResourceIdentifier。感觉我们是在映射,或者pipeResourceIdentifier。
- 所有缓存是否都应该使用带有请求键的weakmap?