查询过滤器... 开发系统时常见的问题。但是当开始编写代码时,每个开发人员都会出现许多熟悉的问题:「我应该把这个查询逻辑放在哪里?我应该如何管理它以方便使用?」。老实说,对于我开发的每个项目,我都会根据以前创建的项目的经验以不同的风格写作。而每次我开始一个新项目,这一次我都会问自己同样的问题,我如何安排查询过滤器!本文可以认为是一个查询过滤系统的逐步开发,有相应的问题。
上下文在撰写本文时,我在 php 8.1 和 mysql 8 上使用 laravel 9。我相信技术栈不是一个大问题,这里我们主要关注构建一个查询过滤器系统。在本文中,我将演示为 users 表构建过滤器。
<?phpuse illuminate\database\migrations\migration;use illuminate\database\schema\blueprint;use illuminate\support\facades\schema;return new class extends migration{ /** * 运行迁移 * * @return void */ public function up() { schema::create('users', function (blueprint $table) { $table->id(); $table->string('name'); $table->string('email')->unique(); $table->string('gender', 10)->nullable()->index(); $table->boolean('is_active')->default(true)->index(); $table->boolean('is_admin')->default(false)->index(); $table->timestamp('birthday')->nullable(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->remembertoken(); $table->timestamps(); }); } /** * 回退迁移 * * @return void */ public function down() { schema::dropifexists('users'); }}
此外,我还使用 laravel telescope 轻松监控查询。
初始点在学习使用 laravel 的第一天,我经常直接在控制器上调用过滤器。简单,没有魔法,容易理解,但是这种方式有问题:
控制器中放置的大量逻辑导致控制器膨胀不能重复使用许多相同的工作重复<?phpnamespace app\http\controllers;use app\models\user;use illuminate\http\request;class usercontroller extends controller{ public function __invoke(request $request) { // /users?name=ryder&email=hartman&gender=male&is_active=1&is_admin=0&birthday=2014-11-30 $query = user::query(); if ($request->has('name')) { $query->where('name', 'like', %{$request->input('name')}%); } if ($request->has('email')) { $query->where('email', 'like', %{$request->input('email')}%); } if ($request->has('gender')) { $query->where('gender', $request->input('gender')); } if ($request->has('is_active')) { $query->where('is_active', $request->input('is_active') ? 1 : 0); } if ($request->has('is_admin')) { $query->where('is_admin', $request->input('is_admin') ? 1 : 0); } if ($request->has('birthday')) { $query->wheredate('birthday', $request->input('birthday')); } return $query->paginate(); // select * from `users` where `name` like '%ryder%' and `email` like '%hartman%' and `gender` = 'male' and `is_active` = 1 and `is_admin` = 0 and date(`birthday`) = '2014-11-30' limit 15 offset 0 }}
使用 local scope为了能够在过滤期间隐藏逻辑,让我们尝试使用 laravel 的 local scope。将查询转换为 user 模型中的函数范围:
// user.phppublic function scopename(builder $query): builder{ if (request()->has('name')) { $query->where('name', 'like', % . request()->input('name') . %); } return $query;}public function scopeemail(builder $query): builder{ if (request()->has('email')) { $query->where('email', 'like', % . request()->input('email') . %); } return $query;}public function scopegender(builder $query): builder{ if (request()->has('gender')) { $query->where('gender', request()->input('gender')); } return $query;}public function scopeisactive(builder $query): builder{ if (request()->has('is_active')) { $query->where('is_active', request()->input('is_active') ? 1 : 0); } return $query;}public function scopeisadmin(builder $query): builder{ if (request()->has('is_admin')) { $query->where('is_admin', request()->input('is_admin') ? 1 : 0); } return $query;}public function scopebirthday(builder $query): builder{ if (request()->has('birthday')) { $query->where('birthday', request()->input('birthday')); } return $query;}// usercontroller.phppublic function __invoke(request $request){ // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11 $query = user::query() ->name() ->email() ->gender() ->isactive() ->isadmin() ->birthday(); return $query->paginate(); // select * from `users` where `name` like '%john%' and `email` like '%desmond%' and `gender` = 'female' and `is_active` = 1 and `is_admin` = 0 and `birthday` = '2015-04-11' limit 15 offset 0}
通过这种设置,我们将大部分数据库操作移到了模型类中,但是代码重复非常多。示例 2 的名称和电子邮件范围过滤器相同,性别生日和 is_active/is_admin 组相同。我们将对类似的查询功能进行分组。
// user.phppublic function scoperelativefilter(builder $query, $inputname): builder{ if (request()->has($inputname)) { $query->where($inputname, 'like', % . request()->input($inputname) . %); } return $query;}public function scopeexactfilter(builder $query, $inputname): builder{ if (request()->has($inputname)) { $query->where($inputname, request()->input($inputname)); } return $query;}public function scopebooleanfilter(builder $query, $inputname): builder{ if (request()->has($inputname)) { $query->where($inputname, request()->input($inputname) ? 1 : 0); } return $query;}// usercontroller.phppublic function __invoke(request $request){ // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11 $query = user::query() ->relativefilter('name') ->relativefilter('email') ->exactfilter('gender') ->booleanfilter('is_active') ->booleanfilter('is_admin') ->exactfilter('birthday'); return $query->paginate(); // select * from `users` where `name` like '%john%' and `email` like '%desmond%' and `gender` = 'female' and `is_active` = 1 and `is_admin` = 0 and `birthday` = '2015-04-11' limit 15 offset 0}
至此,我们已经对大部分重复项进行了分组。但是,删除 if 语句或将这些过滤器扩展到另一个模型有点困难。我们正在寻找一种方法来彻底解决这个问题。
使用管道设计模式管道设计模式是一种设计模式,它提供了逐步构建和执行一系列操作的能力。 laravel 有内置的 pipeline 让我们可以很容易地在实际中应用这种设计模式,但由于某种原因,它没有在官方文档中列出。 laravel 本身也将 pipeline 应用于请求和响应之间的中间件。最基本的,要在 laravel 中使用 pipeline,我们可以这样使用
app(\illuminate\pipeline\pipeline::class) ->send($intialdata) ->through($pipes) ->thenreturn(); // data with pipes applied
对于我们的问题,可以将初始查询 user:query() 传递给 pipeline,通过过滤器步骤,并返回应用过滤器的查询构建器。
// usercontrollerpublic function __invoke(request $request){ // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11 $query = app(pipeline::class) ->send(user::query()) ->through([ // filters ]) ->thenreturn(); return $query->paginate();
现在我们需要构建管道过滤器:
// file: app/models/pipes/relativefilter.php<?phpnamespace app\models\pipes;use illuminate\database\eloquent\builder;class relativefilter{ public function __construct(protected string $inputname) { } public function handle(builder $query, \closure $next) { if (request()->has($this->inputname)) { $query->where($this->inputname, 'like', % . request()->input($this->inputname) . %); } return $next($query); }}// file: app/models/pipes/exactfilter.php<?phpnamespace app\models\pipes;use illuminate\database\eloquent\builder;class exactfilter{ public function __construct(protected string $inputname) { } public function handle(builder $query, \closure $next) { if (request()->has($this->inputname)) { $query->where($this->inputname, request()->input($this->inputname)); } return $next($query); }}//file: app/models/pipes/booleanfilter.php<?phpnamespace app\models\pipes;use illuminate\database\eloquent\builder;class booleanfilter{ public function __construct(protected string $inputname) { } public function handle(builder $query, \closure $next) { if (request()->has($this->inputname)) { $query->where($this->inputname, request()->input($this->inputname) ? 1 : 0); } return $next($query); }}// usercontrollerpublic function __invoke(request $request){ // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11 $query = app(pipeline::class) ->send(user::query()) ->through([ new \app\models\pipes\relativefilter('name'), new \app\models\pipes\relativefilter('email'), new \app\models\pipes\exactfilter('gender'), new \app\models\pipes\booleanfilter('is_active'), new \app\models\pipes\booleanfilter('is_admin'), new \app\models\pipes\exactfilter('birthday'), ]) ->thenreturn(); return $query->paginate(); // select * from `users` where `name` like '%john%' and `email` like '%desmond%' and `gender` = 'female' and `is_active` = 1 and `is_admin` = 0 and `birthday` = '2015-04-11' limit 15 offset 0}
通过将每个查询逻辑移动到一个单独的类,我们解锁了使用 oop 的定制可能性,包括多态、继承、封装、抽象。比如你在 pipeline 的 handle 函数中看到,只有 if 语句中的逻辑不同,我会通过创建抽象类 basefilter 的方式将其分离抽象出来
//file: app/models/pipes/basefilter.php<?phpnamespace app\models\pipes;use illuminate\database\eloquent\builder;abstract class basefilter{ public function __construct(protected string $inputname) { } public function handle(builder $query, \closure $next) { if (request()->has($this->inputname)) { $query = $this->apply($query); } return $next($query); } abstract protected function apply(builder $query): builder;}// booleanfilterclass booleanfilter extends basefilter{ protected function apply(builder $query): builder { return $query->where($this->inputname, request()->input($this->inputname) ? 1 : 0); }}// exactfilterclass exactfilter extends basefilter{ protected function apply(builder $query): builder { return $query->where($this->inputname, request()->input($this->inputname)); }}// relativefilterclass relativefilter extends basefilter{ protected function apply(builder $query): builder { return $query->where($this->inputname, 'like', % . request()->input($this->inputname) . %); }}
现在我们的过滤器直观且高度可重用,易于实现甚至扩展,只需创建一个管道,扩展 basefilter 并声明函数 apply 即可应用到 pipeline.中。
将 local scope 与 pipeline 相结合此时,我们将尝试在控制器上隐藏 pipeline,通过在 model 中创建一个调用 pipeline 的作用域来使我们的代码更简洁。
// user.phppublic function scopefilter(builder $query){ $criteria = $this->filtercriteria(); return app(\illuminate\pipeline\pipeline::class) ->send($query) ->through($criteria) ->thenreturn();}public function filtercriteria(): array{ return [ new \app\models\pipes\relativefilter('name'), new \app\models\pipes\relativefilter('email'), new \app\models\pipes\exactfilter('gender'), new \app\models\pipes\booleanfilter('is_active'), new \app\models\pipes\booleanfilter('is_admin'), new \app\models\pipes\exactfilter('birthday'), ];}// usercontroller.phppublic function __invoke(request $request){ // /users?name=john&email=desmond&gender=female&is_active=1&is_admin=0&birthday=2015-04-11 return user::query() ->filter() ->paginate() ->appends($request->query()); // 将所有当前查询附加到分页链接中 // select * from `users` where `name` like '%john%' and `email` like '%desmond%' and `gender` = 'female' and `is_active` = 1 and `is_admin` = 0 and `birthday` = '2015-04-11' limit 15 offset 0}
用户现在可以从任何地方调用过滤器。但是其他模型也想实现过滤,我们将创建一个包含范围的 trait,并在模型内部声明参与过滤过程的 pipeline。
// user.phpuse app\models\concerns\filterable;class user extends authenticatable { use filterable; protected function getfilters() { return [ new \app\models\pipes\relativefilter('name'), new \app\models\pipes\relativefilter('email'), new \app\models\pipes\exactfilter('gender'), new \app\models\pipes\booleanfilter('is_active'), new \app\models\pipes\booleanfilter('is_admin'), new \app\models\pipes\exactfilter('birthday'), ]; } // 其余代码// file: app/models/concerns/filterable.phpnamespace app\models\concerns;use illuminate\database\eloquent\builder;use illuminate\pipeline\pipeline;trait filterable{ public function scopefilter(builder $query) { $criteria = $this->filtercriteria(); return app(pipeline::class) ->send($query) ->through($criteria) ->thenreturn(); } public function filtercriteria(): array { if (method_exists($this, 'getfilters')) { return $this->getfilters(); } return []; }}
我们已经解决了分而治之的问题,每个文件,每个类,每个函数现在都有明确的职责。代码也干净、直观且更易于重用,不是吗!我把这个帖子 demo 整个流程的代码都放在这里了。
结语以上是我构建高级查询过滤器系统的一部分,同时向你介绍了一些 laravel 编程方法,例如 local scope 尤其是 pipeline 设计模式。要快速轻松地将此设置应用于新项目,你可以使用包 pipeline query collection,其中包括一组预构建的管道,使其易于安装和使用。希望大家多多支持!
原文地址:https://baro.rezonia.com/blog/building-a-sexy-query-filter
译文地址:https://learnku.com/laravel/t/68762
更多编程相关知识,请访问:编程视频!!
以上就是手把手教你实现一个 laravel 查询过滤器的详细内容。