galviadigital/json-api

适用于 Laravel 的轻量级 JSON:API 资源

1.0.0 2023-08-10 13:28 UTC

This package is auto-updated.

Last update: 2024-09-10 15:53:29 UTC


README

JSON:API Resource: a Laravel package by Tim MacDonald

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

安装

您可以通过 composerPackagist 安装。

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。在我们的用户资源中,将公开用户的 namewebsitetwitter_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 规范的一个功能,允许客户端指定他们希望接收的任何给定资源类型的哪些属性。这允许得到更确定的响应,同时提高服务器端性能并减少有效载荷大小。稀疏字段集为您的资源开箱即用。

我们在这里简要介绍它们,但我们建议阅读规范以了解更多信息。

例如,假设我们正在构建一个博客索引页。该页面将显示每篇文章的 titleexcerpt,以及作者的名字。如果客户端希望,他们可以限制响应,仅包含每个资源类型的所需属性,并排除其他属性,例如文章的 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" 是一种反模式

致谢

特别感谢 Caneco 为我们的标志提供的帮助 ✨

v1 todo

  • 服务器实现重新思考。
  • 重新思考对象和属性的命名
  • 所有内容使用驼峰命名法
  • 允许资源指定其JsonResource类。
  • 将所有缓存设置为WeakMaps。
  • 必须在构造函数中首先处理的事项。请参阅链接:href