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 { 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 { 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 { public function index() { $posts = Post::limit(10)->get(); return PostResource::collection($posts); } }
A filtragem padrão é organizada pelo seguinte código:
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 { protected $request; protected $builder; public function __construct(Request $request) { $this->request = $request; } 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); } } } 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 PostFilterAgora, 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 { public function status(string $status) { $this->builder->where('post_status', strtolower($status)); } 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
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 { 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 { 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