使用 Dingo API 快速构建 RESTful API(六)—— 转化器及响应构建器的高级使用

转化器的高级功能

除了我们上篇教程介绍的一些基本使用之外,还可以通过传递额外的参数实现一些更高级的转化器功能。

资源键

我们可以在响应构建器的 itemcollectionpaginator 方法传入一个参数数组作为第三个参数,在这个数组中,可以通过 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 规范的数据格式:

Dingo Transformer JsonApiSerializer

同时也会应用第三个参数设置的资源键值作为返回资源的数据类型。关于 Fractal 支持的其它序列化器可以参考之前的 Fractal 教程

设置游标

此外,学院君前面介绍 Fractal 对分页数据进行处理的时候讲到,对于结果集很大的、对性能要求严苛的系统,除了分页器之外,还可以通过游标获取分页数据,同样,也可以在第四个回调函数中设置游标来转化分页数据,下面我们改写 Api\TaskControllerindex 方法实现代码如下:

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 Transformer 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 请求,当你的转化层需要查询字符串参数或者其他相关数据时会用到它。

自定义转化层类定义好了之后,可以将其配置到 .envAPI_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 构造函数的第一个参数来设置:

Dingo Response 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 基类,同时还要实现 formatEloquentModelformatEloquentCollectionformatArray 以及 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\TaskControllerindex 方法实现代码如下:

public function index(Request $request)
{
    $limit = $request->input('limit') ? : 10;
    $tasks = Task::paginate($limit);
    return $this->response->paginator($tasks, new TaskTransformer());
}

在 Postman 中访问该方法对应路由,就可以看到相应结果:

Dingo Response Event

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

下一篇: 使用 Dingo API 快速构建 RESTful API(七)—— 错误及异常处理