现在服务端程序员的主要工作已经不再是套模版,而是编写基于 JSON 的 API 接口。可惜大家编写接口的风格往往迥异,这就给系统集成带来了很多不必要的沟通成本,如果你有类似的困扰,那么不妨关注一下 JSONAPI,它是一个基于 JSON 构建 API 的规范标准,一个简单的 API 接口大致如下所示:
简单说明一下:根节点中的 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 JsonSerializable; use Illuminate\Http\Resources\Json\JsonResource; 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(JsonResource $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(JsonResource $resource) { $result = [ 'type' => $resource->type, ]; $data = $resource->resolve(); $link = env('APP_URL') . "/{$resource->type}/{$data['id']}"; foreach ($data as $key => $value) { if ($value instanceof JsonResource) { if ($value instanceof ResourceCollection) { foreach ($value as $v) { $result['relationships'][$key]['data'][] = $this->compressResource($v); } } else { $result['relationships'][$key]['data'] = $this->compressResource($value); } $result['relationships'][$key]['links'] = [ 'self' => "{$link}/relationships/{$key}", 'related' => "{$link}/{$key}", ]; } elseif ($key == 'id') { $result['id'] = (string) $value; } else { $result['attributes'][$key] = $value; } } $result['links'] = [ 'self' => $link, ]; $result = array_merge($result, $resource->with(request())); $result += $resource->additional; return $result; } protected function compressResource(JsonResource $resource) { $included = $this->serialize($resource); $key = $resource->type . ':' . $included['id']; if (! isset($this->included[$key])) { $this->included[$key] = $included; } return [ 'type' => $resource->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 App\Http\Resource; class ArticleResource extends Resource { public $type = 'articles'; public function toArray($request) { return [ 'id' => $this->id, 'name' => $this->name, 'author' => new AuthorResource($this->whenLoaded('author')), ]; } } ?>
对应的父类如下,重定义它的原因在于避免显式调用 JsonApiSerializer:
<?php namespace App\Http; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Http\Resources\Json\JsonResource; use App\Http\Serializers\JsonApiSerializer; abstract class Resource extends JsonResource { public $type; public static function collection($resource) { return new ResourceCollection($resource, static::class); } public function toResponse($request) { return new JsonApiSerializer($this); } public function __toString() { return json_encode(new JsonApiSerializer($this)); } } class ResourceCollection extends AnonymousResourceCollection { public function toResponse($request) { return new JsonApiSerializer($this); } public function __toString() { return json_encode(new JsonApiSerializer($this)); } } ?>
对应的 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 $resource; } } ?>
整个过程没有对 Laravel 的架构进行太大的侵入,可以说是目前 Laravel 实现 JSONAPI 的最优解决方案了,有兴趣的可以研究一下 JsonApiSerializer 的实现,虽然只有一百多行代码,但是我却费了好大的力气才实现,可以说是行行皆辛苦啊。
评论前必须登录!
注册