查詢過(guò)濾器… 開(kāi)發(fā)系統(tǒng)時(shí)常見(jiàn)的問(wèn)題。但是當(dāng)開(kāi)始編寫(xiě)代碼時(shí),每個(gè)開(kāi)發(fā)人員都會(huì)出現(xiàn)許多熟悉的問(wèn)題:「我應(yīng)該把這個(gè)查詢邏輯放在哪里?我應(yīng)該如何管理它以方便使用?」。老實(shí)說(shuō),對(duì)于我開(kāi)發(fā)的每個(gè)項(xiàng)目,我都會(huì)根據(jù)以前創(chuàng)建的項(xiàng)目的經(jīng)驗(yàn)以不同的風(fēng)格寫(xiě)作。而每次我開(kāi)始一個(gè)新項(xiàng)目,這一次我都會(huì)問(wèn)自己同樣的問(wèn)題,我如何安排查詢過(guò)濾器!本文可以認(rèn)為是一個(gè)查詢過(guò)濾系統(tǒng)的逐步開(kāi)發(fā),有相應(yīng)的問(wèn)題。
上下文
在撰寫(xiě)本文時(shí),我在 PHP 8.1 和 MySQL 8 上使用 Laravel 9。我相信技術(shù)棧不是一個(gè)大問(wèn)題,這里我們主要關(guān)注構(gòu)建一個(gè)查詢過(guò)濾器系統(tǒng)。在本文中,我將演示為 users 表構(gòu)建過(guò)濾器。
<?php use IlluminateDatabaseMigrationsMigration; use IlluminateDatabaseSchemaBlueprint; use IlluminateSupportFacadesSchema; return new class extends Migration { /** * 運(yùn)行遷移 * * @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 輕松監(jiān)控查詢。
初始點(diǎn)
在學(xué)習(xí)使用 Laravel 的第一天,我經(jīng)常直接在控制器上調(diào)用過(guò)濾器。簡(jiǎn)單,沒(méi)有魔法,容易理解,但是這種方式有問(wèn)題:
- 控制器中放置的大量邏輯導(dǎo)致控制器膨脹
- 不能重復(fù)使用
- 許多相同的工作重復(fù)
<?php namespace AppHttpControllers; use AppModelsUser; use IlluminateHttpRequest; 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
為了能夠在過(guò)濾期間隱藏邏輯,讓我們嘗試使用 Laravel 的 Local Scope。將查詢轉(zhuǎn)換為 User 模型中的函數(shù)范圍:
// User.php public 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.php public 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 }
通過(guò)這種設(shè)置,我們將大部分?jǐn)?shù)據(jù)庫(kù)操作移到了模型類中,但是代碼重復(fù)非常多。示例 2 的名稱和電子郵件范圍過(guò)濾器相同,性別生日和 is_active/is_admin 組相同。我們將對(duì)類似的查詢功能進(jìn)行分組。
// User.php public 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.php public 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 }
至此,我們已經(jīng)對(duì)大部分重復(fù)項(xiàng)進(jìn)行了分組。但是,刪除 if 語(yǔ)句或?qū)⑦@些過(guò)濾器擴(kuò)展到另一個(gè)模型有點(diǎn)困難。我們正在尋找一種方法來(lái)徹底解決這個(gè)問(wèn)題。
使用管道設(shè)計(jì)模式
管道設(shè)計(jì)模式是一種設(shè)計(jì)模式,它提供了逐步構(gòu)建和執(zhí)行一系列操作的能力。 Laravel 有內(nèi)置的 Pipeline 讓我們可以很容易地在實(shí)際中應(yīng)用這種設(shè)計(jì)模式,但由于某種原因,它沒(méi)有在官方文檔中列出。 Laravel 本身也將 Pipeline 應(yīng)用于請(qǐng)求和響應(yīng)之間的中間件。最基本的,要在 Laravel 中使用 Pipeline,我們可以這樣使用
app(IlluminatePipelinePipeline::class) ->send($intialData) ->through($pipes) ->thenReturn(); // data with pipes applied
對(duì)于我們的問(wèn)題,可以將初始查詢 User:query() 傳遞給 pipeline,通過(guò)過(guò)濾器步驟,并返回應(yīng)用過(guò)濾器的查詢構(gòu)建器。
// UserController public 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();
現(xiàn)在我們需要構(gòu)建管道過(guò)濾器:
// File: app/Models/Pipes/RelativeFilter.php <?php namespace AppModelsPipes; use IlluminateDatabaseEloquentBuilder; 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 <?php namespace AppModelsPipes; use IlluminateDatabaseEloquentBuilder; 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 <?php namespace AppModelsPipes; use IlluminateDatabaseEloquentBuilder; 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); } } // UserController public 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 AppModelsPipesRelativeFilter('name'), new AppModelsPipesRelativeFilter('email'), new AppModelsPipesExactFilter('gender'), new AppModelsPipesBooleanFilter('is_active'), new AppModelsPipesBooleanFilter('is_admin'), new AppModelsPipesExactFilter('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 }
通過(guò)將每個(gè)查詢邏輯移動(dòng)到一個(gè)單獨(dú)的類,我們解鎖了使用 OOP 的定制可能性,包括多態(tài)、繼承、封裝、抽象。比如你在 pipeline 的 handle 函數(shù)中看到,只有 if 語(yǔ)句中的邏輯不同,我會(huì)通過(guò)創(chuàng)建抽象類 BaseFilter 的方式將其分離抽象出來(lái)
//File: app/Models/Pipes/BaseFilter.php <?php namespace AppModelsPipes; use IlluminateDatabaseEloquentBuilder; 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; } // BooleanFilter class BooleanFilter extends BaseFilter { protected function apply(Builder $query): Builder { return $query->where($this->inputName, request()->input($this->inputName) ? 1 : 0); } } // ExactFilter class ExactFilter extends BaseFilter { protected function apply(Builder $query): Builder { return $query->where($this->inputName, request()->input($this->inputName)); } } // RelativeFilter class RelativeFilter extends BaseFilter { protected function apply(Builder $query): Builder { return $query->where($this->inputName, 'like', "%" . request()->input($this->inputName) . "%"); } }
現(xiàn)在我們的過(guò)濾器直觀且高度可重用,易于實(shí)現(xiàn)甚至擴(kuò)展,只需創(chuàng)建一個(gè)管道,擴(kuò)展 BaseFilter 并聲明函數(shù) apply 即可應(yīng)用到 Pipeline.中。
將 Local Scope 與 Pipeline 相結(jié)合
此時(shí),我們將嘗試在控制器上隱藏 Pipeline,通過(guò)在 Model 中創(chuàng)建一個(gè)調(diào)用 Pipeline 的作用域來(lái)使我們的代碼更簡(jiǎn)潔。
// User.php public function scopeFilter(Builder $query) { $criteria = $this->filterCriteria(); return app(IlluminatePipelinePipeline::class) ->send($query) ->through($criteria) ->thenReturn(); } public function filterCriteria(): array { return [ new AppModelsPipesRelativeFilter('name'), new AppModelsPipesRelativeFilter('email'), new AppModelsPipesExactFilter('gender'), new AppModelsPipesBooleanFilter('is_active'), new AppModelsPipesBooleanFilter('is_admin'), new AppModelsPipesExactFilter('birthday'), ]; } // UserController.php public 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()); // 將所有當(dāng)前查詢附加到分頁(yè)鏈接中 // 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 }
用戶現(xiàn)在可以從任何地方調(diào)用過(guò)濾器。但是其他模型也想實(shí)現(xiàn)過(guò)濾,我們將創(chuàng)建一個(gè)包含范圍的 Trait,并在模型內(nèi)部聲明參與過(guò)濾過(guò)程的 Pipeline。
// User.php use AppModelsConcernsFilterable; class User extends Authenticatable { use Filterable; protected function getFilters() { return [ new AppModelsPipesRelativeFilter('name'), new AppModelsPipesRelativeFilter('email'), new AppModelsPipesExactFilter('gender'), new AppModelsPipesBooleanFilter('is_active'), new AppModelsPipesBooleanFilter('is_admin'), new AppModelsPipesExactFilter('birthday'), ]; } // 其余代碼 // File: app/Models/Concerns/Filterable.php namespace AppModelsConcerns; use IlluminateDatabaseEloquentBuilder; use IlluminatePipelinePipeline; 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 []; } }
我們已經(jīng)解決了分而治之的問(wèn)題,每個(gè)文件,每個(gè)類,每個(gè)函數(shù)現(xiàn)在都有明確的職責(zé)。代碼也干凈、直觀且更易于重用,不是嗎!我把這個(gè)帖子 Demo 整個(gè)流程的代碼都放在這里了。
結(jié)語(yǔ)
以上是我構(gòu)建高級(jí)查詢過(guò)濾器系統(tǒng)的一部分,同時(shí)向你介紹了一些 Laravel 編程方法,例如 Local Scope 尤其是 Pipeline 設(shè)計(jì)模式。要快速輕松地將此設(shè)置應(yīng)用于新項(xiàng)目,你可以使用包 Pipeline Query Collection,其中包括一組預(yù)構(gòu)建的管道,使其易于安裝和使用。希望大家多多支持!
原文地址:https://baro.rezonia.com/blog/building-a-sexy-query-filter
譯文地址:https://learnku.com/laravel/t/68762