QueryFilter: le concept de modèles de filtrage

Je souhaite attirer votre attention sur le concept d'organisation du filtrage par demande d'URL. Par exemple, j'utiliserai le langage PHP et le framework Laravel.

Concept


L'idée est de créer une classe QueryFilter universelle pour travailler avec des filtres.

GET /posts?title=source&status=active 

En utilisant cet exemple, nous filtrerons les publications (modèle de publication) selon les critères suivants:

  • La présence du mot "source" dans le champ titre ;
  • La valeur "publier" dans le champ d' état ;

Exemple d'application


Modèle de poste

 <?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'id', 'title', 'slug', 'status', 'type', 'published_at', 'updated_at', ]; } 

Ajouter un itinéraire:

 Route::get('/posts', 'PostController@index'); 

Créez un fichier Resource \ Post pour la sortie au format JSON :

 namespace App\Http\Resources; use Illuminate\Http\Resources\Json\JsonResource; class Post extends JsonResource { /** * Transform the resource into an array. * * @param \Illuminate\Http\Request $request * @return array */ public function toArray($request) { return [ 'id' => $this->ID, 'title' => $this->post_title, 'slug' => $this->post_name, 'status' => $this->post_status, 'type' => $this->post_type, 'published_at' => $this->post_date, 'updated_at' => $this->post_modified, ]; } } 

Et le contrôleur lui-même avec une seule action:

 namespace App\Http\Controllers; use App\Http\Resources\Post as PostResource; use App\Post; class PostController extends Controller { /** * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection */ public function index() { $posts = Post::limit(10)->get(); return PostResource::collection($posts); } } 

Le filtrage standard est organisé par le code suivant:

 /** * @param Request $request * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection */ public function index(Request $request) { $query = Post::limit(10); if ($request->filled('status')) { $query->where('post_status', $request->get('status')); } if ($request->filled('title')) { $title = $request->get('title'); $query->where('post_title', 'like', "%$title%"); } $posts = $query->get(); return PostResource::collection($posts); } 

Avec cette approche, nous sommes confrontés à la croissance du contrôleur, ce qui n'est pas souhaitable.

Implémentation de QueryFilter


La signification d'un tel concept est d'utiliser une classe distincte pour chaque entité qui mappe les méthodes à chaque champ pour le filtrage.

Filtrer par demande:

 GET /posts?title=source&status=publish 

Pour le filtrage, nous aurons la classe PostFilter et les méthodes title () et status () . PostFilter étendra la classe abstraite QueryFiler qui est responsable de la correspondance des méthodes de classe avec les paramètres passés.

Méthode applicable ()

La classe QueryFIlter possède une méthode apply () , qui est chargée d' appeler les filtres qui se trouvent dans la classe enfant PostFilter .

 namespace App\Http\Filters; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; abstract class QueryFilter { /** * @var Request */ protected $request; /** * @var Builder */ protected $builder; /** * @param Request $request */ public function __construct(Request $request) { $this->request = $request; } /** * @param Builder $builder */ public function apply(Builder $builder) { $this->builder = $builder; foreach ($this->fields() as $field => $value) { $method = camel_case($field); if (method_exists($this, $method)) { call_user_func_array([$this, $method], (array)$value); } } } /** * @return array */ protected function fields(): array { return array_filter( array_map('trim', $this->request->all()) ); } } 

L'essentiel est que pour chaque champ passé par Request, nous avons une méthode distincte dans la classe de filtre enfant (classe PostFilter). Cela nous permet de personnaliser la logique de chaque champ de filtre.

Classe PostFilter

Passons maintenant à la création d'une classe PostFilter qui étend QueryFilter . Comme mentionné précédemment, cette classe doit contenir des méthodes pour chaque champ par lequel nous devons filtrer. Dans notre cas, les méthodes title ($ value) et status ($ value)

 namespace App\Http\Filters; use Illuminate\Database\Eloquent\Builder; class PostFilter extends QueryFilter { /** * @param string $status */ public function status(string $status) { $this->builder->where('post_status', strtolower($status)); } /** * @param string $title */ public function title(string $title) { $words = array_filter(explode(' ', $title)); $this->builder->where(function (Builder $query) use ($words) { foreach ($words as $word) { $query->where('post_title', 'like', "%$word%"); } }); } } 

Ici, je ne vois aucune raison pour une analyse détaillée de chacune des méthodes, des requêtes assez standard. Le fait est que nous avons maintenant une méthode distincte pour chaque champ et que nous pouvons utiliser toute logique dont nous avons besoin pour former la demande.

Créer scopeFilter ()

Maintenant, nous devons lier le concepteur de modèle et de requête

 /** * @param Builder $builder * @param QueryFilter $filter */ public function scopeFilter(Builder $builder, QueryFilter $filter) { $filter->apply($builder); } 

Pour effectuer une recherche, nous devons appeler la méthode filter () et passer une instance de QueryFilter , dans notre cas PostFilter .

 $filteredPosts = Post::filter($postFilter)->get(); 

Ainsi, toute la logique de filtrage est traitée en appelant la méthode de filtrage ($ postFilter) , sauvant le contrôleur d'une logique inutile.

Pour faciliter la réutilisation, vous pouvez mettre la méthode scopeFilter dans le trait et l'utiliser pour chaque modèle qui doit être filtré.

 namespace App\Http\Filters; use Illuminate\Database\Eloquent\Builder; trait Filterable { /** * @param Builder $builder * @param QueryFilter $filter */ public function scopeFilter(Builder $builder, QueryFilter $filter) { $filter->apply($builder); } } 

Dans la publication, ajoutez:

 class Post extends CorcelPost { use Filterable; 

Il reste à ajouter index () à la méthode du contrôleur en tant que paramètre PostFilter et appeler la méthode du modèle filter () .

 class PostController extends Controller { /** * @param PostFilter $filter * @return \Illuminate\Http\Resources\Json\ResourceCollection */ public function index(PostFilter $filter) { $posts = Post::filter($filter)->limit(10)->get(); return PostResource::collection($posts); } } 

C’est tout. Nous avons déplacé toute la logique de filtrage vers la classe appropriée, en respectant le principe de responsabilité unique (S dans le système de principes SOLID)

Conclusion


Cette approche de la mise en œuvre des filtres vous permet de vous en tenir au contrôleur fin de remorque, et facilite également la rédaction des tests.

Voici un exemple utilisant PHP et Laravel. Mais comme je l'ai dit, c'est un concept qui peut fonctionner avec n'importe quel langage ou framework.

Les références


Source: https://habr.com/ru/post/fr485520/


All Articles