QueryFilter: das Konzept des Filterns von Modellen

Ich möchte Sie auf das Konzept der Filterung nach URL-Anforderungen aufmerksam machen. Zum Beispiel werde ich die PHP-Sprache und das Laravel-Framework verwenden.

Konzept


Die Idee ist, eine universelle QueryFilter- Klasse für die Arbeit mit Filtern zu erstellen.

GET /posts?title=source&status=active 

In diesem Beispiel werden Beiträge (Beitragsmodell) nach den folgenden Kriterien gefiltert:

  • Das Vorhandensein des Wortes "Quelle" im Titelfeld;
  • Der Wert "Publizieren" im Statusfeld ;

Anwendungsbeispiel


Beitragsmodell

 <?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', ]; } 

Route hinzufügen:

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

Erstellen Sie eine Resource \ Post- Datei für die Ausgabe im JSON-Format :

 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, ]; } } 

Und die Steuerung selbst mit einer einzigen Aktion:

 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); } } 

Die Standardfilterung ist nach folgendem Code organisiert:

 /** * @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); } 

Bei diesem Ansatz sehen wir uns mit dem unerwünschten Wachstum des Controllers konfrontiert.

Implementieren von QueryFilter


Die Bedeutung eines solchen Konzepts besteht darin, für jede Entität eine eigene Klasse zu verwenden, die jedem Feld Methoden zum Filtern zuordnet.

Auf Anfrage filtern:

 GET /posts?title=source&status=publish 

Zum Filtern stehen die PostFilter- Klasse sowie die Methoden title () und status () zur Verfügung. PostFilter erweitert die abstrakte Klasse QueryFiler, die für den Abgleich von Klassenmethoden mit übergebenen Parametern verantwortlich ist.

Methode anwenden ()

Die QueryFIlter- Klasse verfügt über eine apply () -Methode, die für das Aufrufen von Filtern verantwortlich ist, die sich in der PostFilter-Kindklasse befinden .

 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()) ); } } 

Im Endeffekt haben wir für jedes Feld, das über Request übergeben wird , eine separate Methode in der untergeordneten Filterklasse (PostFilter-Klasse). Auf diese Weise können wir die Logik für jedes Filterfeld anpassen.

PostFilter- Klasse

Nun wollen wir eine PostFilter- Klasse erstellen, die QueryFilter erweitert . Wie bereits erwähnt, muss diese Klasse Methoden für jedes Feld enthalten, nach denen gefiltert werden muss. In unserem Fall die Methoden title ($ value) und 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%"); } }); } } 

Hier sehe ich keinen Grund für eine detaillierte Analyse der einzelnen Methoden, ganz normale Abfragen. Der Punkt ist, dass wir jetzt für jedes Feld eine separate Methode haben und jede Logik verwenden können, die wir zum Bilden der Anfrage benötigen.

ScopeFilter () erstellen

Jetzt müssen wir den Modell- und Abfrage-Designer binden

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

Um zu suchen, müssen wir die filter () -Methode aufrufen und eine Instanz von QueryFilter übergeben , in unserem Fall PostFilter .

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

Daher wird die gesamte Filterlogik durch Aufrufen der Filtermethode ($ postFilter) verarbeitet , wodurch der Controller vor unnötiger Logik geschützt wird .

Um die Wiederverwendung zu vereinfachen, können Sie die scopeFilter- Methode in das Merkmal einfügen und für jedes Modell verwenden, das gefiltert werden muss.

 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); } } 

In Post hinzufügen:

 class Post extends CorcelPost { use Filterable; 

Es bleibt übrig, der Controller-Methode index () als PostFilter- Parameter hinzuzufügen und die Modellmethode filter () aufzurufen.

 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); } } 

Das ist alles. Wir haben die gesamte Filterlogik in die entsprechende Klasse verschoben und dabei das Prinzip der Einzelverantwortung beachtet (S im SOLID-Prinzipensystem).

Fazit


Dieser Ansatz zur Implementierung von Filtern ermöglicht es Ihnen, sich an den Trailer Thin Controller zu halten, und erleichtert auch das Schreiben von Tests.

Hier ist ein Beispiel mit PHP und Laravel. Aber wie gesagt, das ist ein Konzept und kann mit jeder Sprache oder jedem Framework funktionieren.

Referenzen


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


All Articles