内容简介:Dingo API 中的转化器(Transformer)有点类似 Laravel 框架自带的API 资源类,都是用于对返回的响应数据进行格式化,通过转化器,你可以轻松实现将对象转化为数组,并支持整型和布尔类型之间的转化,以及分页结果和嵌套关联。这篇教程我们主要讨论转化器在 Dingo API 中的使用,这里的转化器包括以下两层意思:在介绍 Dingo 转化器使用之前,有必要大致了解下其底层实现原理。
转化器简介
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 League\Fractal\Pagination\PagerfantaPaginatorAdapter League\Fractal\Pagination\PhalconFrameworkPaginatorAdapter League\Fractal\Pagination\ZendFrameworkPaginatorAdapter
至于为什么使用分页适配器,是为了将不同框架实现的分页器转化为符合 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 中的使用。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 使用 Dingo API 扩展包快速构建 Laravel RESTful API(六) —— 转化器及响应构建器的高级使用
- 使用 Dingo API 扩展包快速构建 Laravel RESTful API(五) —— 转化器篇(下):结合响应构建器构...
- RecyclerView使用指南(一)—— 基本使用
- 如何使用Meteorjs使用URL参数
- 使用 defer 还是不使用 defer?
- 使用 Typescript 加强 Vuex 使用体验
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。