欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

深入浅析JSONAPI在PHP中的应用

程序员文章站 2022-05-07 15:01:44
现在服务端程序员的主要工作已经不再是套模版,而是编写基于 json 的 api 接口。可惜大家编写接口的风格往往迥异,这就给系统集成带来了很多不必要的沟通成本,如果你有类似...

现在服务端程序员的主要工作已经不再是套模版,而是编写基于 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中的应用,希望对大家有所帮助