现在服务端程序员的主要工作已经不再是套模版,而是编写基于 JSON 的 API 接口。可惜大家编写接口的风格往往迥异,这就给系统集成带来了很多不必要的沟通成本,如果你有类似的困扰,那么不妨关注一下 JSONAPI ,它是一个基于 JSON 构建 API 的规范标准,一个简单的 API 接口大致如下所示:
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\\MissingValue; use Illuminate\\Http\\Resources\\Json\\Resource; use Illuminate\\Http\\Resources\\Json\\ResourceCollection; use Illuminate\\Pagination\\AbstractPaginator; class JsonApiSerializer implements \\JsonSerializable { protected $resource; protected $resourceValue; protected $data = []; protected static $included = []; public function __construct($resource, $resourceValue) { $this->resource = $resource; $this->resourceValue = $resourceValue; } public function jsonSerialize() { foreach ($this->resourceValue as $key => $value) { if ($value instanceof Resource) { $this->serializeResource($key, $value); } else { $this->serializeNonResource($key, $value); } } if (!$this->isRootResource()) { return $this->data; } $result = [ \'data\' => $this->data, ]; if (static::$included) { $result[\'included\'] = static::$included; } if (!$this->resource->resource instanceof AbstractPaginator) { return $result; } $paginated = $this->resource->resource->toArray(); $result[\'links\'] = $this->links($paginated); $result[\'meta\'] = $this->meta($paginated); 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; } } protected function links($paginated) { return [ \'first\' => $paginated[\'first_page_url\'] ?? null, \'last\' => $paginated[\'last_page_url\'] ?? null, \'prev\' => $paginated[\'prev_page_url\'] ?? null, \'next\' => $paginated[\'next_page_url\'] ?? null, ]; } protected function meta($paginated) { return [ \'current_page\' => $paginated[\'current_page\'] ?? null, \'from\' => $paginated[\'from\'] ?? null, \'last_page\' => $paginated[\'last_page\'] ?? null, \'per_page\' => $paginated[\'per_page\'] ?? null, \'to\' => $paginated[\'to\'] ?? null, \'total\' => $paginated[\'total\'] ?? null, ]; } protected function isRootResource() { return isset($this->resource->isRoot) && $this->resource->isRoot; } } ?>
对应的 Resource 基本还和以前一样,只是返回值改了一下:
<?php namespace App\\Http\\Resources; use App\\Article; 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\' => $this->whenLoaded(\'author\'), ]; return new JsonApiSerializer($this, $value); } } ?>
对应的 Controller 也和原来差不多,只是加入了一个 isRoot 属性,用来识别根:
<?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->isRoot = true; return $resource; } } ?>
整个过程没有对 Laravel 的架构进行太大的侵入,可以说是目前 Laravel 实现 JSONAPI 的最优解决方案了,有兴趣的可以研究一下 JsonApiSerializer 的实现,虽然只有一百多行代码,但是我却费了好大的力气才实现,可以说是行行皆辛苦啊。
总结
以上所述是小编给大家介绍的JSONAPI在PHP中的应用,希望对大家有所帮助,如果大家有任何疑问欢迎给我留言,小编会及时回复大家的!
暂无评论内容