Query filter... Common problems when developing systems. But when starting to write code, there are many familiar questions that arise for every developer: "Where should I put this query logic? How should I manage it for ease of use?". Honestly, for every project I develop, I write in a different style based on my experience with previous projects I created. And every time I start a new project, this time I ask myself the same question, how do I arrange query filters! This article can be considered as a step-by-step development of a query filtering system, with corresponding issues.
At the time of writing this article, I am using Laravel 9 on PHP 8.1 and MySQL 8. I believe the technology stack is not a big issue, here we mainly focus on building a query filter system. In this article, I will demonstrate building a filter for the users table.
<?php use 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'); } }
Additionally, I use Laravel Telescope to easily monitor queries.
On my first days of learning to use Laravel, I often called filters directly on the controller. Simple, no magic, easy to understand, but there are problems with this approach:
<?php namespace 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 } }
To be able to hide logic during filtering, let's try using Laravel's Local Scope. Convert the query to a function scope in the User model:
// 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 }
With this setup, we move most of the database operations into the model class, but there is a lot of code duplication. Example 2 has the same name and email range filter, same gender birthday and is_active/is_admin group. We'll group similar query functions.
// 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 }
At this point, we have grouped most of the duplicates. However, it's a bit difficult to remove the if statements or extend these filters to another model. We are looking for a way to completely resolve this issue.
The Pipeline Design Pattern is a design pattern that provides the ability to build and perform a sequence of operations step by step. Laravel has built-in Pipeline that makes it easy for us to apply this design pattern in practice, but for some reason it is not listed in the official documentation. Laravel itself also uses Pipelines as middleware between requests and responses. At its most basic, to use a Pipeline in Laravel, we can use
app(\Illuminate\Pipeline\Pipeline::class) ->send($intialData) ->through($pipes) ->thenReturn(); // data with pipes applied
For our problem, we can pass the initial query User:query() to the pipeline, through the filter step, and back to apply the filter query builder.
// 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();
Now we need to build the pipeline filter:
// File: app/Models/Pipes/RelativeFilter.php <?php namespace 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 <?php namespace 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 <?php namespace 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); } } // 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 \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 }
By moving each query logic to a separate class, we unlock customization possibilities using OOP, including polymorphism, inheritance , encapsulation, abstraction. For example, you can see in the handle function of the pipeline that only the logic in the if statement is different. I will separate and abstract it by creating the abstract class BaseFilter
//File: app/Models/Pipes/BaseFilter.php <?php namespace 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; } // 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) . "%"); } }
Now our filter is intuitive and highly reusable , easy to implement or even extend, just create a pipeline, extend BaseFilter and declare the function apply to apply to Pipeline.
At this point, we will try to hide Pipeline on the controller by creating a scope in Model that calls Pipeline. Make our code more concise.
// User.php public 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.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()); // 将所有当前查询附加到分页链接中 // 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 }
Users can now invoke filters from anywhere. But other models also want to implement filtering, we will create a Trait containing the scope, and declare the Pipeline involved in the filtering process inside the model.
// User.php use 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.php namespace 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 []; } }
We have solved the divide and conquer problem, every file, every class, every function now has a clear responsibility. The code is also clean, intuitive and easier to reuse, isn’t it! I put the code of the entire process of this post Demo here.
The above is part of my way of building an advanced query filter system, and also introduces you to some Laravel programming methods, such as Local Scope and especially the Pipeline design pattern. To quickly and easily apply this setup to new projects, you can use the package Pipeline Query Collection, which includes a set of pre-built pipelines, making them easy to install and use. I hope you all will support me!
Original address: https://baro.rezonia.com/blog/building-a-sexy-query-filter
Translation address: https://learnku.com/ laravel/t/68762
For more programming-related knowledge, please visit: Programming Video! !
The above is the detailed content of Teach you step by step how to implement a Laravel query filter. For more information, please follow other related articles on the PHP Chinese website!