使用 Dingo API 快速构建 RESTful API(六)—— 转化器及响应构建器的高级使用
转化器的高级功能
除了我们上篇教程介绍的一些基本使用之外,还可以通过传递额外的参数实现一些更高级的转化器功能。
资源键
我们可以在响应构建器的 item
、collection
或 paginator
方法传入一个参数数组作为第三个参数,在这个数组中,可以通过 key
传入 Fractal 资源的类型标识:
return $this->item($task, new TaskTransformer, ['key' => 'task']);
但是这个功能只有在 Fractal 的序列化器为 JsonApiSerializer
时才有效。关于 Fractal 系列化器的设置我们下面会提到。
使用回调
Fractal 转化层允许你注册一个创建资源之后触发的回调,这个回调接收League\Fractal\Resource\Item
或者 League\Fractal\Resource\Collection
资源作为第一个参数,League\Fractal\Manager
实例作为第二个参数,在这个回调函数中我们可以自定义序列化器、设置游标、以及其它与 Fractal 交互的功能。
设置序列化器
比如我们设置序列化器可以这么做(默认使用的序列化器是 DataArraySerializer
):
// 引入命名空间
use League\Fractal\Serializer\JsonApiSerializer;
$task = Task::findOrFail($id);
return $this->response->item($task, new TaskTransformer(), ['key' => 'task'], function ($resource, $fractal) {
$fractal->setSerializer(new JsonApiSerializer());
});
这样,我们访问对应的资源路由,返回的响应就变成了遵循 JSON-API 规范的数据格式:
同时也会应用第三个参数设置的资源键值作为返回资源的数据类型。关于 Fractal 支持的其它序列化器可以参考之前的 Fractal 教程。
设置游标
此外,学院君前面介绍 Fractal 对分页数据进行处理的时候讲到,对于结果集很大的、对性能要求严苛的系统,除了分页器之外,还可以通过游标获取分页数据,同样,也可以在第四个回调函数中设置游标来转化分页数据,下面我们改写 Api\TaskController
的 index
方法实现代码如下:
public function index(Request $request)
{
$current = $request->input('current');
$previous = $request->input('previous');
$limit = $request->input('limit') ? : 10;
if ($current) {
$tasks = Task::where('id', '>', $current)->take($limit)->get();
} else {
$tasks = Task::take($limit)->get();
}
$next = $tasks->last()->id;
$cursor = new Cursor($current, $previous, $next, $tasks->count());
return $this->response->collection($tasks, new TaskTransformer, [], function ($resource, $fractal) use ($cursor) {
$resource->setCursor($cursor);
});
}
再次访问首页路由,就可以看到通过游标转化的分页数据了:
自定义转化层
Dingo API 默认基于 Fractal 转化层实现转化器,你可以在 config/api.php
中看到这个默认配置:
'transformer' => env('API_TRANSFORMER', Dingo\Api\Transformer\Adapter\Fractal::class),
如果你不想使用 Fractal 作为转化层,可以在 Dingo API 中创建自己的转化层。要实现自定义的转化层,需要创建一个继承自 Dingo\Api\Contract\Transformer\Adapter
基类的子类,并且实现 transform
方法:
use Dingo\Api\Http\Request;
use Dingo\Api\Transformer\Binding;
use Dingo\Api\Contract\Transformer\Adapter;
class MyCustomTransformerAdapter implements Adapter
{
public function transform($response, $transformer, Binding $binding, Request $request)
{
// 调用转化层对响应数据进行转化
}
}
transform
是唯一必须实现的方法(因为基类中只声明了这个抽象方法),你也可以添加自己需要的其他方法,就像 Dingo\Api\Transformer\Adapter\Fractal
那样。transform
方法的目的是获取$response
,然后和 $transformer
一起传递给转化层处理,转化层处理之后会返回一个数组,最终这个数组被 transform
方法返回。如果你的转化层很简单,可以完全在这个方法中实现所有逻辑。
$bindings
参数用于支持更多特性,比如添加元数据或者允许开发者通过回调与转化层交互。
$request
参数是当前的 HTTP 请求,当你的转化层需要查询字符串参数或者其他相关数据时会用到它。
自定义转化层类定义好了之后,可以将其配置到 .env
的 API_TRANSFORMER
配置项作为全局默认转化层,也可以在运行时通过如下方式动态设置:
$this->app['Dingo\Api\Transformer\Factory']->setAdapter(function ($app) {
return new MyCustomTransformerAdapter(...);
});
自定义响应数据格式
默认情况下,Dingo API 返回的响应会自动使用 JSON 格式并设置相应的 Content-Type
头。除了 JSON 之外 Dingo 还支持 JSONP 格式,该格式会将响应封装到一个回调中,要将响应数据默认格式调整为该格式,只需要简单将配置文件 config/api.php
中的默认 JSON 格式替换成 JSONP 即可:
'defaultFormat' => env('API_DEFAULT_FORMAT', 'json'),
'formats' => [
'json' => Dingo\Api\Http\Response\Format\Jsonp::class,
],
默认情况下回调参数默认查询字符串是 callback
,这可以通过修改 Jsonp
构造函数的第一个参数来设置:
如果查询字符串不包含任何参数将会返回 JSON 响应。当然,你还可以在运行时动态设置返回数据格式:
use Dingo\Api\Http\Response;
public function show($id)
{
$task = Task::findOrFail($id);
Response::addFormatter('json', new Response\Format\Jsonp);
return $this->response->item($task, new TaskTransformer());
}
如果 Dingo 自带的 JSON 和 JSONP 格式满足不了你的需求,还可以自定义数据格式类。自定义的数据格式类需要继承自 Dingo\Api\Http\Response\Format\Format
基类,同时还要实现 formatEloquentModel
、formatEloquentCollection
、formatArray
以及 getContentType
方法。
Morphing 和 Morphed 事件
在 Dingo API 发送响应之前会先对该响应数据进行转化(morph),这个过程包括运行所有转换器(Transformer)以及通过配置的响应数据格式发送响应。如果你需要控制响应数据如何被转化可以使用 Dingo 提供的 ResponseIsMorphing
(转化前触发)和 ResponseWasMorphed
(转化后触发)事件。
我们可以在 app/Listeners
目录下为上述事件创建监听器:
php artisan make:listener AddPaginationLinksToResponse
编辑 app/Listeners/AddPaginationLinksToResponse.php
代码如下:
<?php
namespace App\Listeners;
use Dingo\Api\Event\ResponseWasMorphed;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
class AddPaginationLinksToResponse
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle(ResponseWasMorphed $event)
{
if (isset($event->content['meta']['pagination'])) {
$links = $event->content['meta']['pagination']['links'];
$next = isset($links['next']) ? $links['next'] : null;
$previous = isset($links['previous']) ? $links['previous'] : null;
$event->response->headers->set(
'link',
sprintf('<%s>; rel="next", <%s>; rel="prev"', $next, $previous)
);
}
}
}
然后通过在 EventServiceProvider
中注册事件及其对应监听器之间的映射关系来监听该事件:
use App\Listeners\AddPaginationLinksToResponse;
use Dingo\Api\Event\ResponseWasMorphed;
protected $listen = [
...
ResponseWasMorphed::class => [
AddPaginationLinksToResponse::class
]
];
现在所有包含分页链接的响应也会将这些链接添加到 Link
响应头。修改 Api\TaskController
的 index
方法实现代码如下:
public function index(Request $request)
{
$limit = $request->input('limit') ? : 10;
$tasks = Task::paginate($limit);
return $this->response->paginator($tasks, new TaskTransformer());
}
在 Postman 中访问该方法对应路由,就可以看到相应结果:
No Comments