Filtre de requête... Questions courantes lors du développement de systèmes. Mais lorsque l'on commence à écrire du code, de nombreuses questions familières se posent à chaque développeur : "Où dois-je placer cette logique de requête ? Comment dois-je la gérer pour en faciliter l'utilisation ?". Honnêtement, pour chaque projet que je développe, j'écris dans un style différent basé sur mon expérience avec les projets précédents que j'ai créés. Et chaque fois que je démarre un nouveau projet, cette fois je me pose la même question : comment organiser les filtres de requêtes ! Cet article peut être considéré comme un développement étape par étape d'un système de filtrage des requêtes, avec les problèmes correspondants.
Au moment de la rédaction de cet article, j'utilise Laravel 9 sur PHP 8.1 et MySQL 8. Je pense que la pile technologique n'est pas un gros problème, ici nous nous concentrons principalement sur la création d'un système de filtrage des requêtes. Dans cet article, je vais démontrer la création d'un filtre pour la table des utilisateurs.
<?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'); } }
De plus, j'utilise le télescope Laravel pour surveiller facilement les requêtes.
Lors de mes premiers jours d'apprentissage à utiliser Laravel, j'appelais souvent des filtres directement sur le contrôleur. Simple, pas de magie, facile à comprendre, mais cette méthode pose des problèmes :
<?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 } }
Pour pouvoir masquer la logique pendant le filtrage, essayons d'utiliser la portée locale de Laravel. Convertissez la requête en portée de fonction dans le modèle User :
// 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 }
Avec cette configuration, nous avons déplacé la plupart des opérations de base de données dans la classe de modèle, mais il y avait beaucoup de duplication de code. L'exemple 2 a le même filtre de nom et de plage d'e-mails, le même sexe d'anniversaire et le même groupe is_active/is_admin. Nous regrouperons les fonctions de requête similaires.
// 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 }
À ce stade, nous avons regroupé la plupart des doublons. Cependant, il est un peu difficile de supprimer les instructions if ou d'étendre ces filtres à un autre modèle. Nous cherchons un moyen de résoudre ce problème une fois pour toutes.
Le modèle de conception de pipeline est un modèle de conception qui offre la possibilité de créer et d'effectuer une séquence d'opérations étape par étape. Laravel a un pipeline intégré qui nous permet d'appliquer facilement ce modèle de conception dans la pratique, mais pour une raison quelconque, il n'est pas répertorié dans la documentation officielle. Laravel lui-même utilise également Pipelines comme middleware entre les requêtes et les réponses. À la base, pour utiliser Pipeline dans Laravel, nous pouvons utiliser
app(\Illuminate\Pipeline\Pipeline::class) ->send($intialData) ->through($pipes) ->thenReturn(); // data with pipes applied
Pour notre problème, nous pouvons transmettre la requête initiale User:query() au pipeline, via l'étape de filtrage, et renvoyer le générateur de requêtes qui applique le filtre. .
// 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();
Maintenant, nous devons construire le filtre pipeline :
// 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 }
En déplaçant chaque logique de requête vers une classe distincte, nous déverrouillons des possibilités de personnalisation à l'aide de la POO, notamment le polymorphisme, l'héritage, l'encapsulation et l'abstraction. Par exemple, vous pouvez voir dans la fonction handle du pipeline que seule la logique de l'instruction if est différente. Je vais la séparer et la résumer en créant la classe abstraite 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) . "%"); } }
Maintenant, notre filtre est intuitif et hautement réutilisable, facile à utiliser. implémentez et même Pour étendre, créez simplement un pipeline, étendez BaseFilter et déclarez la fonction apply pour l'appliquer au Pipeline.
À ce stade, nous allons essayer de masquer le pipeline sur le contrôleur pour rendre notre code plus propre en créant une portée dans le modèle qui appelle le pipeline.
// 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 }
Les utilisateurs peuvent désormais appeler des filtres de n'importe où. Mais d'autres modèles souhaitent également implémenter le filtrage, nous allons créer un Trait qui contient la portée et déclarer le Pipeline qui participe au processus de filtrage à l'intérieur du modèle.
// 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 []; } }
Nous avons résolu le problème de diviser pour régner, chaque fichier, chaque classe, chaque fonction a désormais une responsabilité claire. Le code est également propre, intuitif et plus facile à réutiliser, n’est-ce pas ! J'ai mis le code de l'ensemble du processus de cet article Démo ici.
Ce qui précède fait partie de ma façon de construire un système avancé de filtrage de requêtes, et vous présente également certaines méthodes de programmation Laravel, telles que Local Scope et en particulier le modèle de conception Pipeline. Pour appliquer rapidement et facilement cette configuration à de nouveaux projets, vous pouvez utiliser le package Pipeline Query Collection, qui comprend un ensemble de pipelines prédéfinis, les rendant faciles à installer et à utiliser. J'espère que vous me soutiendrez tous !
Adresse originale : https://baro.rezonia.com/blog/building-a-sexy-query-filter
Adresse de traduction : https://learnku.com/laravel/t/68762
Plus pour connaissances liées à la programmation, veuillez visiter : Vidéo de programmation ! !
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!