timacdonald/json-api

Laravel 的轻量级 JSON:API 资源

v1.0.0-beta.7 2024-03-13 01:31 UTC

README

JSON:API Resource: a Laravel package by Tim MacDonald

JSON:API 资源用于 Laravel

这是一个轻量级的 Laravel API 资源,它帮助您遵守 JSON:API 标准,支持稀疏字段集、复合文档等。

注意 这些文档不是为了向您介绍 JSON:API 规范及其相关概念而设计的,如果您还不熟悉它,应该转到并阅读规范。以下文档仅涵盖如何通过该包实现规范。

目录

版本支持

  • PHP: 8.1, 8.2, 8.3
  • Laravel: 9.0, 10.0, 11.0

安装

您可以使用来自 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
{
    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 规范的一个特性,允许客户端指定他们希望为任何给定资源类型接收哪些属性。这允许提供更确定的响应,同时提高服务器端性能并减少有效负载大小。稀疏字段集对您的资源来说可以开箱即用。

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

例如,假设我们正在为博客构建一个索引页面。页面将显示每篇帖子的 titleexcerpt,以及帖子的作者的 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 是一种反模式"

致谢

特别感谢(Caneco)为标志✨所做的贡献 Caneco

v1 待办事项

  • 服务器实现重新思考。
  • 重新思考对象和属性的命名。
  • 所有内容都使用驼峰命名法。
  • 允许资源指定它们的JsonResource类。
  • 将所有缓存改为WeakMaps。
  • 某些“必须”在构造函数中首先执行的事情。请参阅链接:href
  • 是否应该使用withResourceIdentifier或mapResourceIdentifier。感觉我们是在映射,或者pipeResourceIdentifier。
  • 所有缓存是否都应该使用带有请求键的weakmap?