使用 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)
等); - 支持复杂数据结构的嵌入和嵌套关联;
- 使用 HAL 和 JSON-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
:资源集合
Item
和 Collection
构造器接收任意你想要发送的数据作为第一个参数,以及一个对应的「转化器」作为第二个参数(对应源码位于 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 接口中有很多可以选择的数据输出格式,最著名的就是 HAL 和 JSON-API,Fractal 默认支持 ArraySerializer
、DataArraySerializer
、JsonApiSerializer
三种序列化器,此外,还支持自定义序列化器。不同的序列化器的区别主要体现在数据命名空间的组织上,通过这些序列化器,你可以在 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
方法来设置序列化器,以上代码返回响应数据格式如下:
再来看下 DataArraySerializer
的数据输出格式,其它代码不变,将序列号器设置那行代码修改如下:
$fractal->setSerializer(new \League\Fractal\Serializer\DataArraySerializer());
对应返回响应输出格式如下,与 ArraySerializer
相比,多出了一层 data
包裹:
需要指出的是,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();
});
对应返回响应的数据格式如下:
使用游标
如果数据结果集特别大,运行 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 的使用我们就简单介绍到这里,更多细节请参考官方文档,下一篇我们将介绍 Dingo API 中如何基于 Fractal 实现转化器以及转化器在 Dingo API 中的使用。
7 Comments
'link'=>route('tag.show',['id'=>$task->id])报错"Route [tag.show] not defined.", 如果把routes/api.php里的 $api->get 改成 Route::get就不报错了 这两种写法有什么说法吗
routes/api.php代码: $api = app(\Dingo\Api\Routing\Router::class);
$api->version('v1',function ($api){
Route::get 就是 Laravel 的路由了 不是 Dingo 的
完整的代码是这样的 你应该是路由定义有问题 要嵌套到version定义里面:
路由routes/api.php代码:
Tag转化器
user转化器
最后用postman访问http://127.0.0.1/dingoapi/fractal/resource/collection报错
显然,你 TagTransformer 里面的 link 也要这么定义:
否则走的就是 Laravel 路由器里的 URL 生成器了
原来如此,哈哈,谢谢啦,搞定了