sheikhheera / requent
一个类似于GraphQL的接口,用于将请求映射到eloquent查询并进行数据转换。
Requires
- php: >=5.6.4
- illuminate/config: >=5.3
- illuminate/database: >=5.3
- illuminate/http: >=5.3
- illuminate/pagination: >=5.3
Requires (Dev)
- fzaninotto/faker: ^1.6
- phpunit/phpunit: 5.7.*
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
,则可以使用find
和first
方法,例如
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
模型只包含id
、title
和body
列,并且只有在用户通过URL查询字符串参数选择帖子时,相关的帖子才会包含在内。
在上述示例中,我们还定义了两个附加方法,user
和comments
。这些方法也是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
模型中的email
和Post
模型中的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, ]; }
查询修改子句
当我们发起请求时,我们可以添加一些查询修改子句,例如,orderBy
、orderByDesc
等。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,