谈谈JSONAPI在PHP中的应用

老王 2017-12-24 11:45

现在服务端程序员的主要工作已经不再是套模版,而是编写基于 JSON 的 API 接口。可惜大家编写接口的风格往往迥异,这就给系统集成带来了很多不必要的沟通成本,如果你有类似的困扰,那么不妨关注一下JSONAPI,它是一个基于 JSON 构建 API 的规范标准,一个简单的 API 接口大致如下所示:

JSONAPI

JSONAPI

简单说明一下:根节点中的 data 用来放置主对象的内容,其中 type 和 id 是必须要有的字段,用来表示主对象的类型和标识,其它简单的属性统统放置到 attributes 里,如果主对象存在一对一、一对多等关联对象,那么放置到 relationships 里,不过只是通过 type 和 id 字段放置一个链接,关联对象的实际内容统统放置在根接点中的 included 里。

有了 JSONAPI,数据解析的过程变得规范起来,节省了不必要的沟通成本。不过如果要手动构建 JSONAPI 数据还是很麻烦的,好在通过使用Fractal可以让实现过程相对自动化一些,上面的例子如果用 Fractal 实现大概是这个样子:

<?php

use League\Fractal\Manager;
use League\Fractal\Resource\Collection;

$articles = [
    [
        'id' => 1,
        'title' => 'JSON API paints my bikeshed!',
        'body' => 'The shortest article. Ever.',
        'author' => [
            'id' => 42,
            'name' => 'John',
        ],
    ],
];

$manager = new Manager();

$resource = new Collection($articles, new ArticleTransformer());

$manager->parseIncludes('author');

$manager->createData($resource)->toArray();

?>

如果让我选最喜爱的 PHP 工具包,Fractal 一定榜上有名,它隐藏了实现细节,让使用者完全不必了解 JSONAPI 协议即可上手。不过如果你想在自己的项目里使用的话,与直接使用 Fractal 相比,可以试试 Fractalistic,它对 Fractal 进行了封装,使其更好用:

<?php

Fractal::create()
   ->collection($articles)
   ->transformWith(new ArticleTransformer())
   ->includeAuthor()
   ->toArray();

?>

如果你是裸写 PHP 的话,那么 Fractalistic 基本就是最佳选择了,不过如果你使用了一些全栈框架的话,那么 Fractalistic 可能还不够优雅,因为它无法和框架本身已有的功能更完美的融合,以 Lavaral 为例,它本身内置了一个API Resources功能,在此基础上我实现了一个 JsonApiSerializer,可以和框架完美融合,代码如下:

<?php

namespace App\Http\Serializers;

use Illuminate\Http\Resources\Json\Resource;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Pagination\AbstractPaginator;

class JsonApiSerializer implements \JsonSerializable
{
    protected $resource;

    protected $data = [];
    protected $included = [];
    protected $links = [];
    protected $meta = [];

    public function __construct(Resource $resource)
    {
        $this->resource = $resource;
    }

    public function jsonSerialize()
    {
        if ($this->resource instanceof ResourceCollection) {
            foreach ($this->resource as $resource) {
                $this->data[] = $this->serialize($resource);
            }
        } else {
            $this->data = $this->serialize($this->resource);
        }

        if ($this->resource->resource instanceof AbstractPaginator) {
            $this->setPagination();
        }

        $result = ['data' => $this->data] + array_filter([
            'included' => array_values($this->included),
            'links' => $this->links,
            'meta' => $this->meta,
        ]);

        return $result;
    }

    protected function serialize(Resource $resource)
    {
        $result = [];

        $data = $resource->resolve();

        $link = url() . "/{$data['type']}/{$data['id']}";

        foreach ($data as $key => $value) {
            if ($value instanceof Resource) {
                if ($value instanceof ResourceCollection) {
                    foreach ($value as $v) {
                        $result['relationships'][$key]['data'][]
                            = $this->compressResource($v);
                    }
                } else {
                    $result['relationships'][$key]['data']
                        = $this->compressResource($v);
                }

                $result['relationships'][$key]['links'] = [
                    // 'self' => "{$link}/relationships/{$key}",
                    'related' => "{$link}/{$key}",
                ];
            } else {
                switch ($key) {
                    case 'id':
                    case 'type':
                        $value = (string)$value;
                    case 'links':
                        $result[$key] = $value;
                        break;
                    default:
                        $result['attributes'][$key] = $value;
                }
            }
        }

        return $result;
    }

    protected function compressResource(Resource $resource)
    {
        $included = $this->serialize($resource);

        $key = "{$included['type']}:{$included['id']}";

        if (! isset($this->included[$key])) {
            $this->included[$key] = $included;
        }

        return [
            'type' => $included['type'],
            'id'   => $included['id'],
        ];
    }

    protected function setPagination()
    {
        $paginated = $this->resource->resource->toArray();

        $this->links = [
            'first' => $paginated['first_page_url'] ?? null,
            'prev' => $paginated['prev_page_url'] ?? null,
            'next' => $paginated['next_page_url'] ?? null,
            'last' => $paginated['last_page_url'] ?? null,
        ];

        $this->meta = [
            'current_page' => $paginated['current_page'] ?? null,
            'last_page' => $paginated['last_page'] ?? null,
            'per_page' => $paginated['per_page'] ?? null,
            'total' => $paginated['total'] ?? null,
            'from' => $paginated['from'] ?? null,
            'to' => $paginated['to'] ?? null,
        ];
    }
}

?>

对应的 Resource 基本还和以前一样,完全没有改变:

<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\Resource;

class ArticleResource extends Resource
{
    public function toArray($request)
    {
        return [
            'type' => 'articles',
            'id' => $this->id,
            'name' => $this->name,
            'author' => new AuthorResource($this->whenLoaded('author')),
        ];
    }
}

?>

对应的 Controller 也和原来差不多:

<?php

namespace App\Http\Controllers;

use App\Article;
use App\Http\Resources\ArticleResource;
use App\Http\Serializers\JsonApiSerializer;

class ArticleController extends Controller
{
    protected $article;

    public function __construct(Article $article)
    {
        $this->article = $article;
    }

    public function show($id)
    {
        $article = $this->article->with('author')->findOrFail($id);

        $resource = new ArticleResource($article);

        return new JsonApiSerializer($resource);
    }
}

?>

整个过程没有对 Laravel 的架构进行太大的侵入,可以说是目前 Laravel 实现 JSONAPI 的最优解决方案了,有兴趣的可以研究一下 JsonApiSerializer 的实现,虽然只有一百多行代码,但是我却费了好大的力气才实现,可以说是行行皆辛苦啊。

[返回] [原文链接]