249建站之家:谈谈JSONAPI在PHP中的应用

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

大连249建站之家免费精品网站自助建设平台免费微信小程序


简单说明一下:根节点中的 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\MissingValue;

use Illuminate\Http\Resources\Json\Resource;

use Illuminate\Http\Resources\Json\ResourceCollection;

use Illuminate\Pagination\AbstractPaginator;


class JsonApiSerializer implements \JsonSerializable

{

    protected $value;

    protected $isRoot;


    protected $data = [];

    protected static $included = [];


    public function __construct($value)

    {

        static $count = 0;


        $this->value = $value;

        $this->isRoot = (++$count == 1);

    }


    public function jsonSerialize()

    {

        foreach ($this->value as $key => $value) {

            if ($value instanceof Resource) {

                $this->serializeResource($key, $value);

            } else {

                $this->serializeNonResource($key, $value);

            }

        }


        if (!$this->isRoot) {

            return $this->data;

        }


        $result = [

            'data' => $this->data,

        ];


        if (static::$included) {

            $result['included'] = static::$included;

        }


        return $result;

    }


    protected function serializeResource($key, $value, $type = null)

    {

        if ($type === null) {

            $type = $key;

        }


        if ($value->resource instanceof MissingValue) {

            return;

        }


        if ($value instanceof ResourceCollection) {

            foreach ($value as $k => $v) {

                $this->serializeResource($k, $v, $type);

            }

        } elseif (is_string($type)) {

            $included = $value->resolve();


            $data = [

                'type' => $included['type'],

                'id' => $included['id'],

            ];


            if (is_int($key)) {

                $this->data['relationships'][$type]['data'][] = $data;

            } else {

                $this->data['relationships'][$type]['data'] = $data;

            }


            static::$included[] = $included;

        } else {

            $this->data[] = $value->resolve();

        }

    }


    protected function serializeNonResource($key, $value)

    {

        switch ($key) {

            case 'id':

                $value = (string)$value;

            case 'type':

            case 'links':

                $this->data[$key] = $value;

                break;

            default:

                $this->data['attributes'][$key] = $value;

        }

    }

}


?>

对应的 Resource 基本还和以前一样,只是返回值改了一下:


<?php


namespace App\Http\Resources;


use Illuminate\Http\Resources\Json\Resource;

use App\Http\Serializers\JsonApiSerializer;


class ArticleResource extends Resource

{

    public function toArray($request)

    {

        $value = [

            'type' => 'articles',

            'id' => $this->id,

            'name' => $this->name,

            'author' => new AuthorResource($this->whenLoaded('author')),

        ];


        return new JsonApiSerializer($value);

    }

}


?>

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


<?php


namespace App\Http\Controllers;


use App\Article;

use App\Http\Resources\ArticleResource;


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);

        // $resource->additional(['meta' => [...]]);


        return $resource;

    }

}


?>

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


推荐

  • QQ空间

  • 新浪微博

  • 人人网

  • 豆瓣

取消