使用 Dingo API 快速构建 RESTful API(四)—— 转化器篇(上):Fractal 简介及其使用入门

转化器简介

Dingo API 中的转化器(Transformer)有点类似 Laravel 框架自带的API 资源类,都是用于对返回的响应数据进行格式化,通过转化器,你可以轻松实现将对象转化为数组,并支持整型和布尔类型之间的转化,以及分页结果和嵌套关联。

这篇教程我们主要讨论转化器在 Dingo API 中的使用,这里的转化器包括以下两层意思:

  • 转化层(transformation layer):准备和处理转化器的库;
  • 转化器(transformer):获取原始数据并将其转化为数组格式的类,转化器的具体处理方式取决于转化层。

在介绍 Dingo 转化器使用之前,有必要大致了解下其底层实现原理。

Fractal 概述

Dingo API 底层使用 Fractal 作为默认的转化层,Fractal 库能够为复杂的数据输出提供表示和转化层,常用于基于 JSON 的 RESTful API,作为一个数据转化层,Fractal 具备以下特点:

  • 在数据源与最终输出数据之间进行隔离,从而避免数据源格式的变化对接口调用方的影响;
  • 提供系统的数据类型转化支持,避免大量的 foreach 和到处进行强制数据类型转化(如 (bool)(int) 等);
  • 支持复杂数据结构的嵌入和嵌套关联;
  • 使用 HALJSON-API 等标准进行数据转化,但也支持自定义格式;
  • 支持对数据结果进行分页;
  • 可以简化 API 接口输出数据构建的复杂性。

为了更好的理解 Dingo 转化器的创建和使用,我们先简单介绍下 Fractal 的使用。

Fractal 使用入门

使用 Fractal 之前,需要先通过 Composer 安装相应的扩展包:

composer require league/fractal

不过,由于我们先前已经安装过 Dingo 扩展包,而 Dingo 扩展包又依赖了 Fractal 扩展包,所以该扩展包已经随着 Dingo 扩展包的安装而安装了,不需要重复安装。

Fractal 有几个术语需要解释,理解了这些术语之后,就基本掌握了 Fractal,从而为 Dingo 转化器的使用打下基础。

资源

所谓「资源」指的是用于表示数据的对象,资源主要分为两类:

  • League\Fractal\Resource\Item:单个资源
  • League\Fractal\Resource\Collection:资源集合

ItemCollection 构造器接收任意你想要发送的数据作为第一个参数,以及一个对应的「转化器」作为第二个参数(对应源码位于 League\Fractal\Resource\ResourceAbstract 基类中):

/**
 * Create a new resource instance.
 *
 * @param mixed                             $data
 * @param callable|TransformerAbstract|null $transformer
 * @param string                            $resourceKey
 */
public function __construct($data = null, $transformer = null, $resourceKey = null)
{
    $this->data = $data;
    $this->transformer = $transformer;
    $this->resourceKey = $resourceKey;
}

转化器是一个用于定义输出数据格式的类或回调函数。下面我们以单个资源为例,在 Laravel 中基于 Fractal 定义一个 API 接口:

Route::get('/fractal/resource/item', function () {
    $task = \App\Task::findOrFail(1);
    $resource = new \League\Fractal\Resource\Item($task, function (\App\Task $task) {
        return [
            'id' => $task->id,
            'text' => $task->text,
            'is_completed' => $task->is_completed ? 'yes' : 'no'
        ];
    });
    $fractal = new \League\Fractal\Manager();
    return $fractal->createData($resource)->toJson();
});

这里我们通过传入闭包函数来定义转化器,关于转化器类的实现后面转化器部分会介绍。如果是集合资源的话,处理方式类似:

Route::get('/fractal/resource/collection', function () {
    $tasks = \App\Task::all();
    $resource = new \League\Fractal\Resource\Collection($tasks, function (\App\Task $task) {
        return [
            'id' => $task->id,
            'text' => $task->text,
            'is_completed' => $task->is_completed ? 'yes' : 'no'
        ];
    });
    $fractal = new \League\Fractal\Manager();
    return $fractal->createData($resource)->toJson();
});

序列化器

在 Fractal 中,我们可以通过设置序列化器来指定数据的转化格式,在 API 接口中有很多可以选择的数据输出格式,最著名的就是 HALJSON-API,Fractal 默认支持 ArraySerializerDataArraySerializerJsonApiSerializer 三种序列化器,此外,还支持自定义序列化器。不同的序列化器的区别主要体现在数据命名空间的组织上,通过这些序列化器,你可以在 Fractal 中快速实现不同数据输出格式的切换,而不需要对转化器做任何修改。

首先我们来看下 ArraySerializer 的数据输出格式:

Route::get('/fractal/serializers', function () {
    $task = \App\Task::findOrFail(1);
    $resource = new \League\Fractal\Resource\Item($task, function (\App\Task $task) {
        return [
            'id' => $task->id,
            'text' => $task->text,
            'is_completed' => $task->is_completed ? 'yes' : 'no'
        ];
    });
    $fractal = new \League\Fractal\Manager();
    $fractal->setSerializer(new \League\Fractal\Serializer\ArraySerializer());
    return $fractal->createData($resource)->toJson();
});

可以看到,我们通过调用 Fractal 管理器实例上的 setSerializer 方法来设置序列化器,以上代码返回响应数据格式如下:

Laravel Fractal

再来看下 DataArraySerializer 的数据输出格式,其它代码不变,将序列号器设置那行代码修改如下:

$fractal->setSerializer(new \League\Fractal\Serializer\DataArraySerializer());

对应返回响应输出格式如下,与 ArraySerializer 相比,多出了一层 data 包裹:

Laravel Fractal

需要指出的是,DataArraySerializer 是 Fractal 默认的数据输出格式。

最后,再看下 JsonApiSerializer 的数据输出格式,还是调整序列号器设置那行代码:

$fractal->setSerializer(new \League\Fractal\Serializer\JsonApiSerializer());

返回响应对应数据格式如下,该格式遵循 JSON-API 标准:

如果以上都不能满足你的需求,还可以创建一个继承自 SerializerAbstract 基类的子类来自定义返回响应的数据格式。

转化器

在「资源」部分,我们已经提到了「转化器」的概念,只是那里是通过回调函数来实现的,只能一次性使用,现在,我们通过独立的类来实现,以提高代码的可复用性。

转化器类必须继承自 League\Fractal\TransformerAbstract 基类,并且至少实现 transform() 方法。我们在代码任务项目中创建一个保存在 app/Transformers 目录下的转化器类 TaskTransformer,并初始化代码如下:

<?php

namespace App\Transformers;

use App\Task;
use League\Fractal\TransformerAbstract;

class TaskTransformer extends TransformerAbstract
{
    public function transform(Task $task)
    {
        return [
            'id' => $task->id,
            'text' => $task->text,
            'completed' => $task->is_completed ? 'yes' : 'no',
            'link' => route('tasks.show', ['id' => $task->id])
        ];
    }
}

这样一来,我们就可以改写之前的资源转化代码如下:

// 获取单个资源
$task = \App\Task::findOrFail(1);
$resource = new \League\Fractal\Resource\Item($task, new \App\Transformers\TaskTransformer());

// 获取资源集合
$tasks = \App\Task::all();
$resources = new \League\Fractal\Resource\Collection($tasks, new \App\Transformers\TaskTransformer());

除此之外,我们还可以在模型字段之外,引入额外的数据,比如关联模型:

<?php

namespace App\Transformers;

use App\Task;
use League\Fractal\TransformerAbstract;

class TaskTransformer extends TransformerAbstract
{
    protected $availableIncludes = ['user'];

    public function transform(Task $task)
    {
        return [
            'id' => $task->id,
            'text' => $task->text,
            'completed' => $task->is_completed ? 'yes' : 'no',
            'link' => route('tasks.show', ['id' => $task->id])
        ];
    }

    public function includeUser(Task $task)
    {
        $user = $task->user;
        return $this->item($user, new UserTransformer());
    }
}

由于在上述代码中引入了新的转化器类 UserTransformer,所以需要创建它:

<?php

namespace App\Transformers;

use App\User;
use League\Fractal\TransformerAbstract;

class UserTransformer extends TransformerAbstract
{
    public function transform(User $user)
    {
        return [
            'id' => $user->id,
            'name' => $user->name
        ];
    }
}

然后修改返回响应数据代码如下,通过 parseIncludes 方法引入要包含的额外字段:

return $fractal->parseIncludes('user')->createData($resource)->toJson();

这样一来,就可以在返回的响应数据中看到 user 字段了:

除此之外,Fractal 还支持引入默认额外字段、排除指定字段、引入 URL 查询参数字段等,更多细节请参考官方文档,这里就不一一列举了。

分页

Fractal 提供了两种解决方案来支持分页数据结果,分别是分页器和游标,下面我们简单演示下如何使用它们。

使用分页器

分页器可以提供丰富的分页结果信息,包括项目总数、上一页/下一页链接等,但相应的代价是可能会带来额外的性能开销,比如每次调用都要统计项目总数,如果对性能要求比较苛刻,可以考虑使用游标来获取分页结果。

当我们使用分页器的时候,创建的分页器类必须实现 League\Fractal\Pagination\PaginatorInterface 接口,然后将实例化后的分页器对象传入 League\Fractal\Resource\Collection::setPaginator() 方法。

为了与当前流行的 PHP 框架兼容,Fractal 提供了以下适配器,方便我们快速在相应的 PHP 框架中集成 Fractal:

  • League\Fractal\Pagination\IlluminatePaginatorAdapter:适配 Laravel 框架的分页器;
  • League\Fractal\Pagination\PagerfantaPaginatorAdapter:适配 Symfony 框架的分页器;
  • League\Fractal\Pagination\PhalconFrameworkPaginatorAdapter:适配 Phalcon 框架的分页器;
  • League\Fractal\Pagination\ZendFrameworkPaginatorAdapter:适配 Zend Framework 的分页器。

至于为什么使用分页适配器,是为了将不同框架实现的分页器转化为符合 Fractal 规范的分页器。

当然,我们这里以 Laravel 框架为例,演示在 Laravel 项目中基于 Fractal 使用分页适配器对分页结果进行处理:

Route::get('fractal/paginator', function () {
    $paginator = \App\Task::paginate();
    $tasks = $paginator->getCollection();

    $resource = new \League\Fractal\Resource\Collection($tasks, new \App\Transformers\TaskTransformer());
    $resource->setPaginator(new \League\Fractal\Pagination\IlluminatePaginatorAdapter($paginator));

    $fractal = new \League\Fractal\Manager();
    return $fractal->createData($resource)->toJson();
});

对应返回响应的数据格式如下:

Fractal分页结果

使用游标

如果数据结果集特别大,运行 select count(*) from sometable 会有很大的性能开销,可以考虑使用游标来分批获取分页结果,游标的使用方式也很简单,和分页器类似,首先需要定义一个实现了 League\Fractal\Pagination\CursorInterface 接口的游标类,实例化之后将对应的游标对象传递到 League\Fractal\Resource\Collection::setCursor() 方法即可。

Fractal 为我们提供了一个非常基础的游标类 League\Fractal\Pagination\Cursor,我们基于它在 Laravel 框架中演示如果通过游标返回分页结果:

Route::get('fractal/cursor', function (Request $request) {
    $current = $request->input('current');
    $previous = $request->input('previous');
    $limit = $request->input('limit', 10);

    if ($current) {
        $tasks = \App\Task::where('id', '>', $current)->take($limit)->get();
    } else {
        $tasks =\App\Task::take($limit)->get();
    }

    $next = $tasks->last()->id;
    $cursor = new \League\Fractal\Pagination\Cursor($current, $previous, $next, $tasks->count());

    $resource = new \League\Fractal\Resource\Collection($tasks, new \App\Transformers\TaskTransformer());
    $resource->setCursor($cursor);

    $fractal = new \League\Fractal\Manager();
    return $fractal->createData($resource)->toJson();
});

通过游标获取分页结果类似限定查询,不会统计项目总数,在性能要优于分页器,上述分页结果返回响应数据格式如下:

Fractal游标分页结果

关于 Fractal 的使用我们就简单介绍到这里,更多细节请参考官方文档,下一篇我们将介绍 Dingo API 中如何基于 Fractal 实现转化器以及转化器在 Dingo API 中的使用。

上一篇: 使用 Dingo API 快速构建 RESTful API(三)—— 返回基本 JSON 响应

下一篇: 使用 Dingo API 快速构建 RESTful API(五)—— 转化器篇(下):结合响应构建器构建 JSON 响应