QueryFilter: o conceito de modelos de filtragem

Quero chamar sua atenção para o conceito de organização da filtragem por solicitação de URL. Por exemplo, usarei a linguagem PHP e o framework Laravel.

Conceito


A idéia é criar uma classe QueryFilter universal para trabalhar com filtros.

GET /posts?title=source&status=active 

Usando este exemplo, filtraremos as postagens (modelo de postagem) pelos seguintes critérios:

  • A presença da palavra "fonte" no campo de título ;
  • O valor "publicar" no campo de status ;

Exemplo de aplicação


Post Model

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

Adicione uma rota:

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

Crie um arquivo Resource \ Post para saída no formato 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, ]; } } 

E o próprio controlador com uma única ação:

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

A filtragem padrão é organizada pelo seguinte código:

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

Com essa abordagem, somos confrontados com o crescimento do controlador, o que é indesejável.

Implementando QueryFilter


O significado desse conceito é usar uma classe separada para cada entidade que mapeia métodos para cada campo para filtragem.

Filtrar por solicitação:

 GET /posts?title=source&status=publish 

Para a filtragem, teremos a classe PostFilter e os métodos title () e status () . O PostFilter estenderá a classe abstrata QueryFiler, responsável por combinar os métodos de classe com os parâmetros passados.

Método apply ()

A classe QueryFIlter possui um método apply () , responsável por chamar os filtros que estão na classe filho 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()) ); } } 

A linha inferior é que, para cada campo passado por Request , temos um método separado na classe de filtro filho (classe PostFilter). Isso nos permite personalizar a lógica para cada campo de filtro.

Classe PostFilter

Agora, vamos criar uma classe PostFilter que estende o QueryFilter . Como mencionado anteriormente, essa classe deve conter métodos para cada campo pelo qual precisamos filtrar. No nosso caso, os métodos title ($ value) e 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%"); } }); } } 

Aqui não vejo razão para uma análise detalhada de cada um dos métodos, consultas bastante padrão. O ponto é que agora temos um método separado para cada campo e podemos usar qualquer lógica necessária para formar a solicitação.

Criar scopeFilter ()

Agora precisamos vincular o designer de modelo e consulta

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

Para pesquisar, precisamos chamar o método filter () e passar uma instância do QueryFilter , no nosso caso PostFilter .

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

Assim, toda a lógica de filtragem é processada chamando o método filter ($ postFilter) , salvando o controlador de lógica desnecessária.

Para facilitar a reutilização, você pode colocar o método scopeFilter na característica e usá-lo para cada modelo que precisa ser filtrado.

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

No Post, adicione:

 class Post extends CorcelPost { use Filterable; 

Resta adicionar index () ao método do controlador como o parâmetro PostFilter e chamar o método de modelo 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); } } 

Só isso. Movemos toda a lógica de filtragem para a classe apropriada, observando o princípio da responsabilidade única (S no sistema de princípios SOLID)

Conclusão


Essa abordagem para a implementação de filtros permite que você se atenha ao controlador fino do trailer e também facilita a gravação de testes.

Aqui está um exemplo usando PHP e Laravel. Mas, como eu disse, esse é um conceito e pode funcionar com qualquer linguagem ou estrutura.

Referências


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


All Articles