tucker-eric / eloquentfilter
以优雅的方式筛选 Eloquent 模型
Requires
- php: >=7.2
- illuminate/config: ~6.0|~7.0|~8.0|~9.0|~10.0|~11.0
- illuminate/console: ~6.0|~7.0|~8.0|~9.0|~10.0|~11.0
- illuminate/database: ~6.0|~7.0|~8.0|~9.0|~10.0|~11.0
- illuminate/filesystem: ~6.0|~7.0|~8.0|~9.0|~10.0|~11.0
- illuminate/pagination: ~6.0|~7.0|~8.0|~9.0|~10.0|~11.0
- illuminate/support: ~6.0|~7.0|~8.0|~9.0|~10.0|~11.0
Requires (Dev)
- mockery/mockery: ^1.3
- phpunit/phpunit: ^8
This package is auto-updated.
Last update: 2024-09-09 16:56:05 UTC
README
以优雅的方式筛选 Eloquent 模型及其关系。
简介
假设我们想要返回一个通过多个参数筛选的用户列表。当我们访问
/users?name=er&last_name=&company_id=2&roles[]=1&roles[]=4&roles[]=7&industry=5
$request->all()
将返回
[ 'name' => 'er', 'last_name' => '', 'company_id' => '2', 'roles' => ['1','4','7'], 'industry' => '5' ]
要筛选所有这些参数,我们需要做类似的事情
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Http\Requests; use App\User; class UserController extends Controller { public function index(Request $request) { $query = User::where('company_id', $request->input('company_id')); if ($request->has('last_name')) { $query->where('last_name', 'LIKE', '%' . $request->input('last_name') . '%'); } if ($request->has('name')) { $query->where(function ($q) use ($request) { return $q->where('first_name', 'LIKE', $request->input('name') . '%') ->orWhere('last_name', 'LIKE', '%' . $request->input('name') . '%'); }); } $query->whereHas('roles', function ($q) use ($request) { return $q->whereIn('id', $request->input('roles')); }) ->whereHas('clients', function ($q) use ($request) { return $q->whereHas('industry_id', $request->input('industry')); }); return $query->get(); } }
使用 Eloquent Filter 筛选相同的输入
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Http\Requests; use App\User; class UserController extends Controller { public function index(Request $request) { return User::filter($request->all())->get(); } }
配置
通过 Composer 安装
composer require tucker-eric/eloquentfilter
有几种方法可以定义模型将使用的筛选器
默认设置
所有筛选器的默认命名空间是 App\ModelFilters\
,并且每个模型都期望筛选器类名遵循 {$ModelName}Filter
命名约定,无论模型所在的命名空间如何。以下是根据默认命名约定提供的模型及其相应筛选器的示例。
Laravel
使用配置文件(可选)
注册服务提供程序将为您提供访问
php artisan model:filter {model}
命令的权限,并允许您发布配置文件。注册服务提供程序不是必需的,只有当您想要更改默认命名空间或使用 artisan 命令时才是必需的
安装 Eloquent Filter 库后,在 config/app.php
配置文件中注册 EloquentFilter\ServiceProvider::class
'providers' => [ // Other service providers... EloquentFilter\ServiceProvider::class, ],
使用发布命令将包配置复制到本地配置中
php artisan vendor:publish --provider="EloquentFilter\ServiceProvider"
在 config/eloquentfilter.php
配置文件中。设置模型筛选器将驻留的命名空间
'namespace' => "App\\ModelFilters\\",
Lumen
注册服务提供程序(可选)
这仅在您想使用
php artisan model:filter
命令时才是必需的。
在 bootstrap/app.php
$app->register(EloquentFilter\LumenServiceProvider::class);
更改默认命名空间
在 bootstrap/app.php
config(['eloquentfilter.namespace' => "App\\Models\\ModelFilters\\"]);
定义默认模型筛选器(可选)
以下内容是可选的。如果模型上没有找到
modelFilter
方法,则将根据 默认命名约定 解决模型的筛选器类
在您的模型中创建一个公开方法 modelFilter()
,它返回 $this->provideFilter(Your\Model\Filter::class);
<?php namespace App; use EloquentFilter\Filterable; use Illuminate\Database\Eloquent\Model; class User extends Model { use Filterable; public function modelFilter() { return $this->provideFilter(\App\ModelFilters\CustomFilters\CustomUserFilter::class); } //User Class }
动态筛选器
您可以通过将筛选器传递给 filter()
方法的第二个参数来动态定义筛选器。动态定义筛选器将优先于为模型定义的任何其他筛选器。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use App\Http\Requests; use App\User; use App\ModelFilters\Admin\UserFilter as AdminFilter; use App\ModelFilters\User\UserFilter as BasicUserFilter; use Auth; class UserController extends Controller { public function index(Request $request) { $userFilter = Auth::user()->isAdmin() ? AdminFilter::class : BasicUserFilter::class; return User::filter($request->all(), $userFilter)->get(); } }
生成筛选器
仅当您已将
EloquentFilter\ServiceProvider::class
注册到 `config/app.php` 中 providers 数组的提供程序时才可用
您可以使用以下 artisan 命令创建模型筛选器
php artisan model:filter User
其中 User
是您为创建筛选器而创建的 Eloquent 模型。这将创建 app/ModelFilters/UserFilter.php
此命令还支持 psr-4 命名空间,用于创建筛选器。您只需确保在类名中转义反斜杠。例如
php artisan model:filter AdminFilters\\User
这将创建 app/ModelFilters/AdminFilters/UserFilter.php
使用
定义筛选器逻辑
根据传递给 filter()
方法的驼峰式输入键定义过滤逻辑。
- 默认情况下,空字符串和null值会被忽略。
- 可以通过在过滤器上设置
protected $allowedEmptyFilters = false;
来配置空字符串和值不被忽略。
- 可以通过在过滤器上设置
- 如果定义了
setup()
方法,它将在任何过滤方法之前被调用一次,无论输入如何。 - 从输入键的末尾删除
_id
以定义方法,因此过滤user_id
将使用user()
方法。- 可以通过在过滤器上定义
protected $drop_id = false;
来更改。
- 可以通过在过滤器上定义
- 没有对应过滤方法的输入会被忽略。
- 键的值将被注入到方法中。
- 所有值都可以通过
$this->input()
方法访问,或者通过键$this->input($key)
获取单个值。 - 在模型过滤类中,所有 Eloquent Builder 方法都可以在
$this
上下文中访问。
要定义以下输入的方法
[ 'company_id' => 5, 'name' => 'Tuck', 'mobile_phone' => '888555' ]
可以使用以下方法
use EloquentFilter\ModelFilter; class UserFilter extends ModelFilter { protected $blacklist = ['secretMethod']; // This will filter 'company_id' OR 'company' public function company($id) { return $this->where('company_id', $id); } public function name($name) { return $this->where(function($q) use ($name) { return $q->where('first_name', 'LIKE', "%$name%") ->orWhere('last_name', 'LIKE', "%$name%"); }); } public function mobilePhone($phone) { return $this->where('mobile_phone', 'LIKE', "$phone%"); } public function setup() { $this->onlyShowDeletedForAdmins(); } public function onlyShowDeletedForAdmins() { if(Auth::user()->isAdmin()) { $this->withTrashed(); } } public function secretMethod($secretParameter) { return $this->where('some_column', true); } }
注意:在上面的示例中,如果您不希望从输入末尾删除
_id
,可以在您的过滤器类上设置protected $drop_id = false
。这样做将允许您拥有company()
过滤方法以及companyId()
过滤方法。
注意:在上面的示例中,如果您不希望将
mobile_phone
映射到mobilePhone()
,可以在您的过滤器类上设置protected $camel_cased_methods = false
。这样做将允许您拥有mobile_phone()
过滤方法而不是mobilePhone()
。默认情况下,可以通过以下输入键调用mobilePhone()
过滤方法:mobile_phone
、mobilePhone
、mobile_phone_id
。
注意:在上面的示例中,所有
setup()
中的方法将在每次在模型上调用filter()
时被调用。
黑名单
在 blacklist
数组中定义的任何方法都不会被过滤器调用。这些方法通常用于内部过滤逻辑。
可以使用 blacklistMethod()
和 whitelistMethod()
方法动态地黑名单和白名单方法。
在上面的示例中,即使输入数组中有 secret_method
键,secretMethod()
也不会被调用。为了调用此方法,它需要被动态白名单。
示例
public function setup() { if(Auth::user()->isAdmin()) { $this->whitelistMethod('secretMethod'); } }
额外的过滤方法
该 Filterable
特性还附带以下查询构建器辅助方法
由于这些方法属于 Filterable
特性,因此可以从实现该特性的任何模型中访问它们,而无需调用模型的 EloquentFilter。
将过滤器应用于模型
在任意 Eloquent 模型上实现 EloquentFilter\Filterable
特性
<?php namespace App; use EloquentFilter\Filterable; use Illuminate\Database\Eloquent\Model; class User extends Model { use Filterable; //User Class }
这为您提供了访问接受输入数组作为参数的 filter()
方法的权限
class UserController extends Controller { public function index(Request $request) { return User::filter($request->all())->get(); } }
通过关系进行过滤
有两种方式可以通过相关模型进行过滤。使用
$relations
数组定义要注入到相关模型过滤器的输入。如果相关模型没有自己的模型过滤器,或者您只想定义如何本地过滤该关系,而不是将该逻辑添加到该模型过滤器中,则可以使用related()
方法通过没有模型过滤器的相关模型进行过滤。您甚至可以将两者结合起来,并定义要使用该模型过滤器的$relations
数组中的哪些输入字段,以及使用related()
方法在相同的关联上定义本地方法。两种方法都将过滤约束嵌套到该关联的同一个whereHas()
查询中。
对于这两个示例,我们将使用以下模型
一个 App\User
,它 hasMany
App\Client::class
class User extends Model { use Filterable; public function clients() { return $this->hasMany(Client::class); } }
并且每个 App\Client
属于 App\Industry::class
class Client extends Model { use Filterable; public function industry() { return $this->belongsTo(Industry::class); } public function scopeHasRevenue($query) { return $query->where('total_revenue', '>', 0); } }
我们希望查询我们的用户,并根据他们过去已产生收入的客户的行业和潜在销量进行过滤。
用于过滤的输入
$input = [ 'industry' => '5', 'potential_volume' => '10000' ];
设置
两种方法都会在关系上调用一个设置查询,每次查询这个关系时都会调用。设置方法的签名是 {$related}Setup()
,并且会注入该关系的查询构建器的实例。对于这个例子,假设当通过客户查询用户时,我只想显示有收入客户的代理商。为了避免在选择方法时没有所有输入而错过整个范围(因为我们可能没有选择正确的方法),并且为了避免查询重复,我们将该约束放在该关系的所有方法上,我们在 UserFilter
中调用相关的设置方法,如下所示:
class UserFilter extends ModelFilter { public function clientsSetup($query) { return $query->hasRevenue(); } }
这将在 UserFilter
运行对 clients()
关系的任何约束时,将 hasRevenue()
预先添加到 clients()
关系查询中。如果没有对 clients()
关系的查询,则不会调用此方法。
有关作用域的更多信息,请参阅此处
过滤相关模型的方法
使用 related()
方法过滤相关模型
related()
方法设置起来稍微简单一些,如果你不会经常使用相关模型的过滤器来显式地过滤该模型,那么它就很好。除了第一个参数是关系名称外,related()
方法接受与 Eloquent.Builder
的 where()
方法相同的参数。
示例
UserFilter
具有使用 ModelFilter
的 related()
方法的 industry()
方法
class UserFilter extends ModelFilter { public function industry($id) { return $this->related('clients', 'industry_id', '=', $id); // This would also be shorthand for the same query // return $this->related('clients', 'industry_id', $id); } public function potentialVolume($volume) { return $this->related('clients', 'potential_volume', '>=', $volume); } }
或者,您甚至可以将一个闭包作为第二个参数传递,该闭包将注入相关模型查询构建器的实例,如下所示:
$this->related('clients', function($query) use ($id) { return $query->where('industry_id', $id); });
使用 $relations
数组过滤相关模型
在 $relations
数组中将关系添加到以模型上引用的关系名称作为键,以及传递给 filter()
方法的输入键数组。
相关的模型 必须 与一个 ModelFilter 关联。我们实例化相关模型的过滤器,并使用从 $relations
数组中获取的输入值来调用相关的方法。
当查询关系表的多列时,这很有用,同时避免了为相同的关系多次调用多个 whereHas()
调用。对于单列,在模型过滤器中使用 $this->whereHas()
方法就足够了。实际上,在底层,模型过滤器在 whereHas()
方法中应用所有约束。
示例
UserFilter
中定义了关系,因此它可以被查询。
class UserFilter extends ModelFilter { public $relations = [ 'clients' => ['industry', 'potential_volume'], ]; }
ClientFilter
中具有用于过滤的 industry
方法
注意:
$relations
数组应标识关系和用于过滤该关系的输入键。正如ModelFilter
的工作方式一样,这将访问该关系过滤器上的驼峰式方法。如果上面的示例使用键industry_type
作为输入,则关系数组将是$relations = ['clients' => ['industry_type']]
,并且ClientFilter
将具有industryType()
方法。
class ClientFilter extends ModelFilter { public $relations = []; public function industry($id) { return $this->where('industry_id', $id); } public function potentialVolume($volume) { return $this->where('potential_volume', '>=', $volume); } }
$relations
数组别名支持
$relations
数组支持别名。当输入不匹配相关模型过滤器的方法时使用。这将转换传递给相关模型过滤器输入的输入键。
示例
class UserFilter extends ModelFilter { public $relations = [ 'clients' => [ 'client_industry' => 'industry', 'client_potential' => 'potential_volume' ] ]; }
上面的示例将接收到一个类似如下的数组:
[ 'client_industry' => 1, 'client_potential' => 100000 ]
并且 ClientFilter
将接收它作为
[ 'industry' => 1, 'potential_volume' => 100000 ]
允许使用更描述性的输入名称,而无需过滤器需要匹配。这允许更多地重用相同的过滤器。
使用两种方法过滤相关模型
您甚至可以使用这两种方法,并且将产生相同的结果,并且只查询相关模型一次。一个例子是
如果以下数组传递给 filter()
方法
[ 'name' => 'er', 'last_name' => '', 'company_id' => 2, 'roles' => [1,4,7], 'industry' => 5, 'potential_volume' => '10000' ]
在 app/ModelFilters/UserFilter.php
<?php namespace App\ModelFilters; use EloquentFilter\ModelFilter; class UserFilter extends ModelFilter { public $relations = [ 'clients' => ['industry'], ]; public function clientsSetup($query) { return $query->hasRevenue(); } public function name($name) { return $this->where(function($q) { return $q->where('first_name', 'LIKE', $name . '%')->orWhere('last_name', 'LIKE', '%' . $name.'%'); }); } public function potentialVolume($volume) { return $this->related('clients', 'potential_volume', '>=', $volume); } public function lastName($lastName) { return $this->where('last_name', 'LIKE', '%' . $lastName); } public function company($id) { return $this->where('company_id',$id); } public function roles($ids) { return $this->whereHas('roles', function($query) use ($ids) { return $query->whereIn('id', $ids); }); } }
向筛选器添加关系值
有时,根据参数的值,可能需要将数据推送到关系筛选器。 push()
方法正是这样做的。它接受一个参数,即键值对数组,或两个参数,即键值对 push($key, $value)
。相关模型将在所有本地值执行后进行筛选,您可以在任何筛选方法中使用此方法。这样可以避免对相关表进行多次查询。例如
public $relations = [ 'clients' => ['industry', 'status'], ]; public function statusType($type) { if($type === 'all') { $this->push('status', 'all'); } }
上面的示例将 'all'
传递给模型 clients
的 status()
方法。
在
setup()
方法中调用push()
方法将允许您向筛选器输入推送值
分页
如果您想分页查询并保留 URL 查询字符串,而无需使用
{!! $pages->appends(Input::except('page'))->render() !!}
paginateFilter()
和 simplePaginateFilter()
方法接受与 Laravel 的分页器相同的输入 Laravel 的分页器,并返回相应的分页器。
class UserController extends Controller { public function index(Request $request) { $users = User::filter($request->all())->paginateFilter(); return view('users.index', compact('users')); }
或者
public function simpleIndex(Request $request) { $users = User::filter($request->all())->simplePaginateFilter(); return view('users.index', compact('users')); } }
在您的视图中,$users->render()
将返回分页链接,就像通常一样,但是如果有空输入且未设置 protected $allowedEmptyFilters
为 false
,则忽略原始查询字符串。
贡献
欢迎任何贡献!