向前一步 —— JSON:API Resourece


JSON:API Resource

上篇教程学院君给大家介绍了如何在 Laravel 项目中统一 REST API 返回的 JSON 数据格式,并引出了业界统一规范 —— JSON API。JSON API 虽好,但是需要服务端承担很多额外开发工作,好在社区已经有人将其封装成基础组件,我们做个拿来主义者直接在项目中使用即可,接下来的两篇教程学院君将以 Laravel 项目为例给大家介绍几个常用的第三方 JSON API 实现扩展包。

很多人可能上手使用的都是上篇教程中介绍的 Laravel 自带的 API Resource,沿着 API Resource 的轨迹往前探索,有一个可以和 API Resource 无缝集成的扩展包实现:timacdonald/json-api,通过它的名字,我们就能直观了解 —— JSON:API Resource for Laravel,JSON API 版本的 Resource 实现。

本小节代码已经提交到 github 仓库:https://github.com/geekr-dev/blog.gitv2 版本。

使用入门

使用该扩展包之前,需要先安装:

composer require timacdonald/json-api

然后让所有之前编写的 API Resource 类改为继承自 JsonApiResource,并编写对应的属性和关联关系映射方法:

<?php

namespace App\Http\Resources;

use App\Models\Comment;
use App\Models\User;
use TiMacDonald\JsonApi\JsonApiResource;

class PostResource extends JsonApiResource
{
    public function toAttributes($request): array
    {
        return [
            'title' => $this->title,
            'slug' => $this->slug,
            'content' => $this->content,
            'views' => $this->views,
            'created_at' => $this->created_at->toDateTimeString(),
            'updated_at' => $this->updated_at->toDateTimeString(),
        ];
    }

    public function toRelationships($request): array
    {
        return [
            'author' => fn () => new UserResource($this->author),
            'comments' => fn () => CommentResource::collection($this->comments),
        ];
    }
}

<?php

namespace App\Http\Resources;

use TiMacDonald\JsonApi\JsonApiResource;

class UserResource extends JsonApiResource
{
    public function toAttributes($request): array
    {
        return [
            'name' => $this->name,
        ];
    }
}


<?php

namespace App\Http\Resources;

use TiMacDonald\JsonApi\JsonApiResource;

class CommentResource extends JsonApiResource
{
    public function toAttributes($request): array
    {
        return [
            'content' => $this->content,
        ];
    }

    public function toRelationships($request): array
    {
        return [
            'author' => fn () => new UserResource($this->author)
        ];
    }
}

其中 toAttributes 方法定义的属性字段会映射 JSON API 的 attributestoRelationships 方法定义的关联资源会映射到 JSON API 的 relationships 以及 included ,而 idtype 由组件底层帮我们自动维护,无需显式设置。

当然,你也可以按照自己的需要自定义 id 和 type 逻辑,细节参考该扩展包的官方文档,这里不详细展开了。

你可以把 JSON:API Resource 扩展包看作是 Laravel API Resource 的一层封装,以让其适配 JSON API 规范(类似一个适配器),其实 JsonApiResource 也是继承了 Illuminate\Http\Resources\Json\JsonResource ,所以使用这个扩展包的好处是除了资源类需要调整代码外,上层调用的代码不用做任何更改,完全兼容原来的 API Resource。

现在我们直接访问 /posts/1 路由,返回结果的 JSON 数据结构如下:

image-20221209111628114

数据结构已经和 JSON API 完全一致了:

  • 资源标识属性:ID和类型
  • 资源主体属性:attributes
  • 关联资源数据:relationships && included

关联资源及链接

咦?怎么没有把关联资源返回?这其实就是 JSON API 的规范之一,所有嵌套关联资源支持按需加载,要想返回,需要通过 include 参数指定,多个关联资源用逗号分隔:

image-20221209113152414

可以看到,我们可以指定想要加载的任意关联资源,还支持嵌套的关联资源,这样一来,接口就变得更加灵活了,除此之外,还为我们后续基于性能维度对接口进行优化提供了更多空间。

不过返回数据资源主体和关联资源的 metalinks 都还是空的,需要自行编写相应的代码,实现方法和 attributesrelationships 一样:

// PostResource
protected function toLinks($request): array
{
    return [
    	Link::self(route('posts.show', $this->id)),
    ];
}

// UserResource
protected function toLinks($request): array
{
    return [
    	Link::self(route('users.show', $this->id)),
    ];
}

// CommentResource
protected function toLinks($request): array
{
    return [
    	Link::self(route('posts.comments.show', [$this->post_id, $this->id]))
    ];
}

一般不关注 meta 字段,可以先不管。

再次访问 /posts/1 路由,这个时候可以看到 included 和资源主体的 links 值有了:

image-20221209123208487

但是 relationships 里面的 links 还是空的:

image-20221209123357386

这是扩展包实现的问题,构建 relationships 返回值数据的时候,底层调用的是 JsonApiResourcetoResourceLink 方法:

/**
     * TODO: @see docs-link
     * @see https://jsonapi.org/format/#document-resource-object-linkage
     */
public function toResourceLink(Request $request): RelationshipLink
{
    return new RelationshipLink(
        new ResourceIdentifier($this->resolveId($request), $this->resolveType($request))
    );
}

可以看到,该方法目前只支持生成 idtype 两个关联资源主体字段,要支持 links 字段,需要在对应关联资源类中重写父类的 toResourceLink 方法。但是对于 comments 这种需要在 data 以外获取某篇文章所有关联评论的链接又该如何设置呢,写到后面就发现又出现各种 trick 和魔术方法了,并且我觉得这类问题应该有组件底层去维护,不应该有开发者通过各种 trick 去修复。

按需显示字段

除了关联关系支持按需获取外,按照 JSON API 规范的说明,JSON 返回数据中的字段也要支持按需获取,这一点 JSON:API Resource 扩展包已经提供了支持:

image-20221209125618586

JSON:API Resource 的问题

除了上面提到的 relationships 中不包含 links 问题之外,关于 JSON API 规范中提到的过滤器、排序、分页等更高阶的 API 接口规范 JSON:API Resource 也没有任何实现,所以虽然相比于 API Resource,JSON:API Resource 扩展包通过封装它向 JSON API 规范前进了一步,但是也是不彻底的,针对一些相对简单的场景我们可以使用它来满足 JSON API 的基本规范,好处是在上层调用的业务代码中完全兼容 API Resource,只需要调整自定义的资源类代码即可,不过如果有完美主义倾向的话,还是需要继续往前探索,找到更完美的 JSON API 实现方案。


Vote Vote Cancel Collect Collect Cancel

<< 上一篇: 从 API Resource 开始

>> 下一篇: 珠联璧合 —— Laravel Query Builder