sheikhheera/requent

一个类似于GraphQL的接口,用于将请求映射到eloquent查询并进行数据转换。

1.0.1 2017-06-26 10:32 UTC

This package is auto-updated.

Last update: 2024-09-20 12:03:52 UTC


README

一个优雅、轻量级的类似GQL(图查询语言)的接口,用于Eloquent,无需配置。它将请求映射到eloquent查询,并根据查询参数转换结果。它还支持使用用户定义的转换器显式地转换查询结果,这提供了一种更安全的方式来以最小的努力交换公共API中的数据。

安装

您可以从终端简单地运行以下命令来安装此包

composer require sheikhheera/requent

或者,在“composer.json”文件的“require”部分中添加以下行,并从终端运行composer install

"sheikhheera/requent": "1.0.*"

如果您使用的是5.5或更高版本,那么您就完成了,不需要在config\app中添加服务提供者或别名。因此,您可以简单地跳过以下步骤,因为Laravel的包自动发现功能。

这将安装包。现在,在您的config/app.php文件中的providers部分添加以下条目

Requent\RequentServiceProvider::class

还要在您的config/app.php文件的aliases部分添加以下条目

'Requent' => Requent\Facade\Requent::class,

如果您一切设置正确,那么您就可以开始使用它而无需任何配置,但您也可以自定义它。

工作原理

此包将使我们能够通过请求查询字符串参数查询资源。例如,如果我们有一个User模型,并且该模型有许多帖子(Post模型),而每个帖子又有许多评论(Comment模型),那么我们可以通过发送以下请求查询具有其帖子以及每个帖子的评论的用户:http://example.com/users?fields=posts{comments}。这是一个最基本的使用案例,但它提供了更多功能。我们还可以通过查询字符串选择每个模型的属性,例如,如果我们只想从User模型中选择电子邮件字段和从Post中选择标题以及从Comment中选择正文,那么我们只需发送以下URL的请求即可

http://example.com/users?fields=email,posts.orderByDesc(id){title,comments{body}}

它将被转换成类似以下的内容(非字面意义)

User::select('email')
->with(['posts' => function($query) {
    $query
    ->orderByDesc('id')
    ->select('title')
    ->with(['comments' => function($query) {
        $query->select('body');
    }])
}]);

基本示例

要使用此包,我们需要创建一些资源(Eloquent模型)。为此,我们将使用同一个想法,使用用户、帖子以及评论模型来演示一个虚构的博客。用户模型通过hasMany关系关联到帖子,帖子模型通过hasMany关系关联到评论。因此,我们需要一个路由,它可以是资源路由,但在这里我们将使用显式路由声明

Route::get('users', 'UserController@index');

现在,我们需要一个控制器,它只是一个简单的控制器,例如

<?php

namespace App\Http\Controllers;

use Requent;
use App\User;
use App\Http\Controllers\Controller;

class UserController extends Controller
{
    public function index()
    {
        return Requent::resource(User::class)->get();
    }
}

现在,我们可以使用以下方式发出请求:http://example.com/users?fields=email,posts{title,comments{body}}。这将给出预期的结果,即一个包含用户的数组(只有来自User的电子邮件列),以及所有相关的帖子(只有来自Post的标题列)和每个帖子的所有评论(只有来自Comment的正文列)。

如果我们想加载任何带有关系但没有选择任何属性的资源,我们只需使用以下请求即可:http://example.com/users?fields=posts{comments}。这是一个最基础的例子,但现在让我们探索一下它的功能。

资源

实际上,一个资源只是一个优雅的模型。在Requent类中,我们应该首先调用的方法是resource,它设置我们要查询的主要资源。因此,我们可以用几种方式设置资源,例如

$resource = Requent::resource(User::class);

此外,我们也可以使用一个对象,例如

$resource = Requent::resource(new User);

我们还可以传递一个查询构建器,例如

$resource = Requent::resource(app(User::class)->where('role', 'admin'));

因此,我们也可以调用任何范围方法,这些方法只是返回一个查询构建器实例。resource方法返回Requent对象,因此我们可以链式调用方法,例如,我们可以调用任何查询执行方法(包括Requent中可用的其他方法),例如

$result = Requent::resource(
    app(User::class)->where('role', 'admin')
)
->transformBy(UserTransformer::class)
->keyBy('users')
->get();

我们将详细了解Requent提供的所有可用方法和功能。让我们继续。

方法

获取

我们之前已经看到了get方法,它只返回一个用户数组,这是

return Requent::resource(User::class)->get();

分页结果

此时,我们将得到一个数组,但我们可以使用相同的get方法检索分页结果,在这种情况下,我们只需要在URL中提供一个查询字符串参数,如下例所示

http://example.com/users?fields=posts{comments}&paginate

此外,我们还可以设置分页器,例如

http://example.com/users?fields=posts{comments}&paginate=simple

这将返回使用SimplePaginator的分页结果,但默认情况下将使用LengthAwarePaginator

每页

我们还可以指定每页我们想要获取的页数,这只是一个参数,例如

http://example.com/users?fields=posts{comments}&paginate=simple&per_page=5

如果我们提供了per_page=n,那么除非我们想使用简单分页器而不是默认分页器,否则我们不需要提供&paginate参数。我们还可以自定义这些参数,我们稍后会查看。

分页

我们还可以直接在Requent上调用paginate方法,例如

return Requent::resource(User::class)->paginate(); // or paginate(10)

简单分页

simplePaginate将返回使用Simple Paginator的分页结果。查看Laravel文档

return Requent::resource(User::class)->simplePaginate(); // or simplePaginate(10)

查找

如果我们想检索单个user,则可以使用findfirst方法,例如

return Requent::resource(User::class)->find($id);

第一个

对于第一个项目,我们可以调用first方法

return Requent::resource(User::class)->first();

获取

这些都是执行查询的可用方法,但还有一个方法叫做fetch。此方法可以返回任何类型的结果,包括集合(数组)、分页结果或单个资源。让我们看看一个例子

// In Controller

public function fetch($id = null)
{
    return Requent::resource(User::class)->fetch($id);
}

要使用此方法,我们需要一个路由,如:Route::get('users/{id?}', 'UserController@fetch')然后我们可以使用这个单独的路由来获取所有类型的结果,例如

获取用户集合(数组)

http://example.com/users?fields=posts{comments}

获取分页结果

http://example.com/users?fields=posts{comments}&paginate=simple&per_page=5

获取单个用户(数组)

http://example.com/users/1?fields=posts{comments}

fetch方法在为资源控制器声明显式路由时非常有用,除了RESTful路由之外。查看Laravel文档

在查询字符串中按模型/资源选择属性时,即:fields=name,posts{title},我们可以使用使用getPropertyAttribute方法定义的动态属性(获取器/访问器)进行选择。请参阅定义访问器的文档。

按资源键选择

集合的查询结果只是一个零索引的数组,但如果我们想的话,我们可以使用keyBy方法将我们的集合包裹在一个键中,例如

return Requent::resource(User::class)->keyBy('users')->get();

这将返回一个用户集合(数组),作为一个键值对,键将是users,结果将是该键的值。我们也可以为单个用户使用键,例如

return Requent::resource(User::class)->keyBy('user')->find(1);

fetch 的情况下,我们可以使用以下类似的方法:

public function fetch($id = null)
{
    return Requent::resource(User::class)->keyBy($id ? 'user' : 'users')->fetch($id);
}

分页的结果将保持不变,默认情况下,Laravel 使用 data 作为键来包装集合。

使用转换器进行数据过滤

转换器的想法来源于 Fractal Transformer 包。这看起来像是重新发明轮子,但实际上并非如此。构建 Requent 包的主要目的是允许通过易于使用的接口从网络应用程序(非公开 API)获取资源/数据,这允许使用任何 javaScript 框架/库读取数据,即使没有定义任何转换器。此外,Eloquent 查询在运行时动态构建以加载所有内容,而 Fractal 使用懒加载。因此,Requent 无法利用 Fractal 提供的数据转换功能。因此,为了提供数据过滤层(用于公开 API),Requent 需要自己的数据过滤机制,但 Fractal 包非常好,我已经在我的项目中独家使用它。

到目前为止,我们已经看到了默认的数据转换,这意味着,用户可以通过通过查询字符串参数 fields 请求任何属性或可用的关系来获取资源,但我们无法在公开 API 使用时保留某些数据为私有。在这里,transformer 就派上用场了。

默认情况下,Requent 使用 DefaultTransformer 类来返回仅选择的属性/关系,例如,如果您使用以下 URL 发送请求:http://example.com/users?fields=email,posts{title,comments},则它将仅返回所选属性/关系。在这种情况下,它将返回您请求的内容,但您可能需要明确定义用户可以通过查询参数从请求中获取哪些属性/关系。为此,您可以创建一个自定义转换器,在其中您可以告诉要返回什么。要创建转换器,您只需通过扩展 Requent\Transformer\Transformer 类来创建转换器类。例如

<?php

namespace App\Http\Transformers;

use Requent\Transformer\Transformer;

class UserTransformer extends Transformer
{
    public function transform($model)
    {
        return [
            'id' => $model->id,
            'name' => $model->name,
            'email' => $model->email,
        ];
    }
}

要使用您自己的自定义转换器,您只需将转换器类传递给 resource 方法即可,即

return Requent::resource(User::class, UserTransformer::class)->fetch($id);

您还可以传递类实例

return Requent::resource(User::class, new UserTransformer)->fetch($id);

您还可以使用 transformBy 方法设置转换器

return Requent::resource(User::class)->transformBy(UserTransformer::class)->fetch($id);

此外,transformBy 方法接受转换器对象实例

return Requent::resource(User::class)->transformBy(new UserTransformer)->fetch($id);

这将通过调用您在转换器类中定义的 transform 方法来转换资源。在这种情况下,transform 方法将用于转换 User 模型,但现在它将不会加载任何关系。这意味着,如果 URL 是类似以下的内容: http://example.com/users?fields=posts{comments},那么只有 User 模型将被转换,结果如下所示

[
    {
        id: 1,
        name: "Aurelio Graham",
        email: "hharvey@example.org"
    },
    {
        id: 2,
        name: "Adolfo Weissnat",
        email: "serena78@example.com",
    }
    // ...
]

要加载从根转换器(例如 UserTransformer)中的任何关系,我们还需要显式声明一个具有与模型中定义的关系相同名称的方法,例如,要加载每个 User 模型相关的帖子,我们需要在我们的 UserTransformer 类中声明一个 posts 方法。例如

class UserTransformer extends Transformer
{
    public function transform($model)
    {
        return [
            'id' => $model->id,
            'name' => $model->name,
            'email' => $model->email,
        ];
    }
    
    // To allow inclussion of posts
    public function posts($model)
    {
        return $this->items($model, PostTransformer::class);
    }
}

在这个示例中,我们为Post模型添加了过滤功能,因此用户可以从URL中选择相关的帖子,例如:http://example.com/users?fields=posts。如果没有在UserTransformer中实现posts方法,用户将无法读取/获取帖子关系。目前我们还没有完成。正如你可以想象的那样,我们正在使用UserTransformer(继承自抽象Transform类)中可用的items方法来转换帖子(Collection),并将另一个转换器(PostTransformer)传递给转换帖子集合。因此,我们需要实现PostTransformer并实现transform方法,在该方法中我们将显式返回每个Post模型的转换数组,例如

namespace App\Http\Transformers;

use Requent\Transformer\Transformer;

class PostTransformer extends Transformer
{
    public function transform($model)
    {
        return [
            'post_id' => $model->id,
            'post_title' => $model->title,
            'post_body' => $model->body,
        ];
    }

    // User can select related user for each Post model
    public function user($model)
    {
        return $this->item($model, new UserTransformer);
    }

    // User can select related comments for each Post model
    public function comments($collection)
    {
        return $this->items($collection, new CommentTransformer);
    }
}

在这个示例中,我们已经为Post模型实现了transform方法以进行响应过滤,因此响应中的Post模型只包含idtitlebody列,并且只有在用户通过URL查询字符串参数选择帖子时,相关的帖子才会包含在内。

在上述示例中,我们还定义了两个附加方法,usercomments。这些方法也是Post模型的关系。user方法定义为belongsTo关系,它简单地映射发布帖子的相关用户,而comments方法加载帖子的相关评论。Post模型看起来像以下内容

namespace App;

use App\User;
use App\Comment;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

根据上述设置,我们可以使用以下URL请求获取所有带有帖子及其相关评论和用户的用户:http://example.com/users?fields=posts{user,comments}

PostTransformer类中,我们在user方法中使用了item方法,该方法实际上接收一个在$model参数中的单个Eloquent模型,因此我们从转换器中调用了item方法。在comments方法中,我们使用了items方法,因为comments方法中的$collection参数接收一个Comment模型的集合。

因此,要允许包含资源中的任何关系,我们必须声明一个同名的方法来声明该关系,并且关系只有在用户在fields参数中选择/包含它时才会包含在内。如果用户选择了一个未通过转换器使用方法公开的资源关系,则它将不会在响应中可用。

用户定义的转换器仅在将转换器类作为resource方法的第二个参数传递或通过调用transformBy方法时用于转换数据,否则,将包含用户请求的结果/响应(如果这些字段/关系在相应的模型中可用)。

获取原始结果

Request有一个raw方法,如果有人不想应用任何转换,这可能很有用,因为转换后返回的数据是一个数组。因此,如果您想执行查询但又想省略默认的数据转换(通过查询字符串选择列),则可以使用raw方法,例如

$result = Requent::resource(User::class)->raw()->fetch($id);

在这种情况下,如果您不提供自定义转换器来转换数据,则请求将使用默认转换器来转换数据。因此,如果您使用http://example.com/users?fields=email,posts{title}进行请求,则它应该只返回User模型中的emailPost模型中的title

在这种情况下,由于raw,请求将执行查询以加载带有指定关系的资源,但它不会过滤结果,因此将返回Eloquent的原始结果(可能是集合、分页数据或模型),作为Requent查询的结果。

转换原始结果

use Requent;
use App\User;
use App\Http\Controllers\Controller;
use Requent\Transformer\TransformerHelper;
use App\Http\Transformers\UserTransformer;

class HomeController extends Controller
{
    use TransformerHelper;
    
    public function index()
    {
        $result = Requent::resource(User::class)->raw()->get();
    
        return $this->transform($result, UserTransformer::class, 'users');
    }
}

在本示例中,传递给转换方法的第三个参数users是可选的,它将用作资源键。

Requent不使用延迟加载,这意味着所有(关系)都是预先加载的,当到达转换器(转换方法)时,查询已经执行完毕,所有内容都已加载。因此,在这个时候,transform方法将接收一个模型,如果我们想加载通过查询未加载的其他关系,那么我们可以在转换它之前简单地调用模型上的load方法,例如

public function transform($model)
{
    if(!$model->relationLoaded('comments')) {
        $model->load('comments');
    }
    
    return [
        'post_id' => $model->id,
        'post_title' => $model->title,
        'post_body' => $model->body,
    ];
}

查询修改子句

当我们发起请求时,我们可以添加一些查询修改子句,例如,orderByorderByDesc等。Requent提供了几个子句用于在URL中使用,如下所示

orderBy

http://blog54.dev/1?fields=posts{user,comments.orderBy(id){user}}

orderByDesc

http://example.com/1?fields=posts{user,comments.orderByDesc(id){user}}

skip & take

http://example.com/1?fields=posts{user,comments.skip(2).take(1){user}}

offset & limit

http://example.com/1?fields=posts{user,comments.offset(2).limit(1){user}}

多个子句

http://example.com/1?fields=posts.orderBy(title).limit(3){user,comments.orderByDesc(id).skip(2).take(1){user}}

当在关系上使用limit/take并加载该关系的集合时,可能会得到错误的结果,因为Laravel在查询上只应用了一次限制。在此处有一个旧问题

自定义

Requent使用一些来自配置文件的基设置。默认情况下,它将按配置运行,但如果你需要修改任何设置,则可以将配置文件从供应商发布到本地应用程序配置目录。要从终端发布配置,请执行以下命令

php artisan vendor:publish --provider="Requent\RequentServiceProvider" --tag="config"

将配置文件发布到您的本地/config目录后,您可以修改任何设置以自定义Requent以满足您的需求。以下代码是从配置文件中提取的,该文件本身就有文档说明。

    /*
    |--------------------------------------------------------------------------
    | Query Identifier
    |--------------------------------------------------------------------------
    |
    | Here you may define the parameter name to pass your query string
    | to select fields/columns. By default, "fields" is set but you may
    | override it if you wish.
    |
    */
    'query_identifier' => 'fields',

    /*
    |--------------------------------------------------------------------------
    | Paginator Identifier
    |--------------------------------------------------------------------------
    |
    | Here you may define the parameter name to get paginated data.
    | By default, "paginate" is set but you may override it if you wish.
    |
    */
    'paginator_identifier' => 'paginate',

    /*
    |--------------------------------------------------------------------------
    | Default Paginator
    |--------------------------------------------------------------------------
    |
    | Here you may define the default paginator to be used when geting paginated
    | result. By default, the length aware paginator will be used unless you override
    | it here or pass the pagination type in the query string.
    |
    | Available Options: "simple", "default"
    |
    */
    'default_paginator' => 'default',

    /*
    |--------------------------------------------------------------------------
    | Per Page Identifier
    |--------------------------------------------------------------------------
    |
    | Here you may define the name of the query string to pass the number
    | of pages you want to retrieve from paginated result. By default, the
    | package will use "per_page" so you can pass ?per_page=10 to get 10 items
    |  per page unless you override it here.
    */
    'per_page_identifier' => 'per_page',

    /*
    |--------------------------------------------------------------------------
    | Default Attributes Selection
    |--------------------------------------------------------------------------
    |
    | Here you may define whether you would like to load all properties/attributes
    | from a model if no property was explicitly selected using the query string. If
    | you just select relations of a model, the package will load all the attributes
    | by default unless you override it here by setting the value to false.
    */
    'select_default_attributes' => true,