发布时间2019-06-19 00:21:28
11
0
0

概述

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

安装

~# composer require league/fractal

资源

[ 资源 ] 指的是 用于表示数据的对象,资源主要分为两类:

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

Item 和 Collection 构造器接收任意你想要发送的数据作为第一个参数,以及一个对应的「转化器」作为第二个参数

/**
 * 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('item',function(){
    $article = \App\Models\Article::findOrFail(1);
    $resource = new \League\Fractal\Resource\Item($article, function(\App\Models\Article $article){
        return [
            'id'    =>  $article->id,
            'text'  =>  $article->text,
            'is_completed'  =>  $article->is_completed ? 'yes' : 'no'
        ];
    });

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

如果是集合资源

Route::get('collection',function(){
    $articles = \App\Models\Article::all();
    $resource = new \League\Fractal\Resource\Collection($articles,function(\App\Models\Article $article){
        return [
            'id'    =>  $article->id,
            'text'  =>  $article->text,
            'is_completed'  =>  $article->is_completed ? 'yes' : 'no'
        ];
    });

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

序列化器

在 Fractal 中,可以通过设置序列化器来指定数据的格式转化。最著名的就是 HALJSON-API,Fractal 默认支持 ArraySerializer 、DataArraySerializer 、JsonApiSerializer 三种序列化器,并且支持自定义序列化器。

ArraySerializer

Route::get('articles',function(){
    ...

    $fractal = new \League\Fractal\Manager();
    //只是在上面基础上添加了 SetSerializer();
    $fractal->setSerializer(new \League\Fractal\Serializer\ArraySerializer());
    return $fractal->createData($resource)->toJson();
});

返回的数据:

//单个资源:
{
    "id": 1,
    "title": "Centos 7 配置环境",
    "describe": "环境版本:mysql5.7.26+php7.2.19+nginx1.12.2\r\n很大部分参考https://laravelacademy.org/post/9696.html;并且踩了一些坑,并列出解决方法,以及不同的解决方法."
}

//资源集合
{
    "data": [
        {
            "id": 1,
            "title": "Centos 7 配置环境",
            "describe": "环境版本:mysql5.7.26+php7.2.19+nginx1.12.2\r\n很大部分参考https://laravelacademy.org/post/9696.html;并且踩了一些坑,并列出解决方法,以及不同的解决方法."
        },
        {
            "id": 2,
            "title": "linux 安装git",
            "describe": "网站部署,通过git直接自动部署。"
        },
        {
            "id": 3,
            "title": "123123",
            "describe": "123123"
        },
        {
            "id": 4,
            "title": "123123123123",
            "describe": "123123123241234"
        }
    ]
}

DataArraySerializer

$fractal->setSerializer(new DataArraySerializer());

输出结果:

//单个资源(多加了一层data):
{
    "data": {
        "id": 1,
        "title": "Centos 7 配置环境",
        "describe": "环境版本:mysql5.7.26+php7.2.19+nginx1.12.2\r\n很大部分参考https://laravelacademy.org/post/9696.html;并且踩了一些坑,并列出解决方法,以及不同的解决方法."
    }
}
//资源合集(没有变化):
{
    "data": [
        {
            "id": 1,
            "title": "Centos 7 配置环境",
            "describe": "环境版本:mysql5.7.26+php7.2.19+nginx1.12.2\r\n很大部分参考https://laravelacademy.org/post/9696.html;并且踩了一些坑,并列出解决方法,以及不同的解决方法."
        },
        {
            "id": 2,
            "title": "linux 安装git",
            "describe": "网站部署,通过git直接自动部署。"
        },
        {
            "id": 3,
            "title": "123123",
            "describe": "123123"
        },
        {
            "id": 4,
            "title": "123123123123",
            "describe": "123123123241234"
        }
    ]
}

JsonApiSerializer

$fractal->setSerializer(new JsonApiSerializer());

返回结果:

//单个资源:
{
    "data": {
        "type": null,
        "id": "1",
        "attributes": {
            "title": "Centos 7 配置环境",
            "describe": "环境版本:mysql5.7.26+php7.2.19+nginx1.12.2\r\n很大部分参考https://laravelacademy.org/post/9696.html;并且踩了一些坑,并列出解决方法,以及不同的解决方法."
        }
    }
}
//资源合集:
{
    "data": [
        {
            "type": null,
            "id": "1",
            "attributes": {
                "title": "Centos 7 配置环境",
                "describe": "环境版本:mysql5.7.26+php7.2.19+nginx1.12.2\r\n很大部分参考https://laravelacademy.org/post/9696.html;并且踩了一些坑,并列出解决方法,以及不同的解决方法."
            }
        },
        {
            "type": null,
            "id": "2",
            "attributes": {
                "title": "linux 安装git",
                "describe": "网站部署,通过git直接自动部署。"
            }
        },
        {
            "type": null,
            "id": "3",
            "attributes": {
                "title": "123123",
                "describe": "123123"
            }
        },
        {
            "type": null,
            "id": "4",
            "attributes": {
                "title": "123123123123",
                "describe": "123123123241234"
            }
        }
    ]
}

自定义 数据格式

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

转换器

转换器必须继承自 League\Fractal\TransformerAbstract 基类,并且至少实现 transform() 方法。

我们在 代码任务中创建一个 保存在 app/Tramsformers 目录下面的 转化器类:

<?php
/**
 * Created by PhpStorm.
 * User: wz
 * Date: 2019/7/20
 * Time: 11:10
 */

namespace App\Transformers;

use App\Models\Article;
use League\Fractal\TransformerAbstract;

class ArticleTransformer extends TransformerAbstract
{
    public function transform(Article $article)
    {
        return [
            'id'    =>  $article->id,
            'title'     =>  $article->title,
            'describe'      =>  $article->describe,
            'content'       =>  $article->content
        ];
    }
}

控制器内调用就可以修改为:

$resource = new Collection($articles,new ArticleTransformer());

关联模型

如果需要获取 文章 对应的发布者 user 的信息,就需要关联模型

//App\Transformers
class ArticleTransformer extends TransformerAbstract
{
    protected $availableIncludes = ['user'];

    ...

    public function includeUser(Article $article)
    {
        $user = $article->user;
        //这里注意,user 后面不能跟 () ,否则会一直报错:
        //Argument 1 passed to App\\Transformers\\UserTransformer::transform() must be an instance of App\\Models\\User, instance of Illuminate\\Database\\Eloquent\\Relations\\BelongsTo given,
        //意思是 transform 第一个参数需要传入模型 user 的实例,给了一个 belongsTo 的实例。
        return $this->item($user,new UserTransformer());
    }
}

//includeUser 函数内的过程很熟悉,跟 控制器内部 获取数据的部分完全一样,所以这里可以看出
//控制器部分 其实分为 上部分为获取数据,下部分仅仅处理格式。

//App\Transformer\UserTransformer
<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2019/7/20
 * Time: 12:08
 */

namespace App\Transformers;

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

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

//App\Http\Controllers\Api\ArticleController
public function index()
{
    ...
    $fractal->setSerializer(new DataArraySerializer());
    return $fractal->parseIncludes('user')->createData($resource)->toJson();
    ...
}

默认额外字段,排除指定字段,引入 URL 查询参数字段等

[官方文档](https://fractal.thephpleague.com/transformers/ 官方文档)

分页

Fractal 提供了两种方案支持分页,分页器 和 游标

分页器

分页器可以提供较多的结果信息,包括 总数,上一页,下一页链接等,但相应的是会有额外的性能开销,比如每次调用都会统计项目总数

创建分页器

必须要实现 League\Fractal\Pagination\PaginatorInterFace 接口,然后将实例化后的分页对象 传入 League\Fractal\Resource\Collection::setPaginator() 方法

    /**
     * Display a listing of the resource.
     *
     * @return \Illuminate\Http\Response
     */
    public function index()
    {
        $paginator = Article::paginate(1);
        $articles = $paginator->getCollection();

        $resource = new Collection($articles,new ArticleTransformer());
        $resource->setPaginator(new IlluminatePaginatorAdapter($paginator));

        $fractal = new Manager();

        $fractal->setSerializer(new ArraySerializer());
        return $fractal->parseIncludes('user')->createData($resource)->toJson();
    }
//返回数据
{
    "data": [
        {
            "id": 1,
            "title": "Centos 7 配置环境",
            "describe": ...,
            "content": ...,
            "user": {
                "id": 1,
                "name": "dsheldon"
            }
        }
    ],
    "meta": {
        "pagination": {
            "total": 4,
            "count": 1,
            "per_page": 1,
            "current_page": 1,
            "total_pages": 4,
            "links": {
                "next": "http://blog.test/api/articles?page=2"
            }
        }
    }
}

为了与当前流行的 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 的分页器。

游标

如果数据集很大的时候,运行 select * from - 会有很大的性能开销。 游标和分页器类似:首先定义一个实现了 Leadue\Fractual\Pagination\CursorInterface() 接口的游标类,实例化之后将对应的游标对象传递到 League\Fractual\Resource\Collection::setCursor()。

    public function index(Request $request)
    {
        $current = $request->input('current');
        $previous = $request->input('previous');
        $limit = $request->input('limit',1);

        if($current){
            $article = Article::where('id','>',$current)->take($limit)->get();
        }else{
            $article = Article::take($limit)->get();
        }

        $next = $article->last()->id;
        $cursor = new Cursor($current,$previous,$next,$article->count());

        $resource = new Collection($article,new ArticleTransformer());
        $resource->setCursor($cursor);

        $fractual = new Manager();
        $fractual->setSerializer(new ArraySerializer());
        return $fractual->parseIncludes('user')->createData($resource)->toJson();
    }