Quiero llamar su atención sobre el concepto de organizar el filtrado por solicitud de URL. Por ejemplo, usaré el lenguaje PHP y el marco Laravel.
Concepto
La idea es crear una clase universal
QueryFilter para trabajar con filtros.
GET /posts?title=source&status=active
Con este ejemplo, filtraremos las
publicaciones (modelo de publicación) según los siguientes criterios:
- La presencia de la palabra "fuente" en el campo del título ;
- El valor "publicar" en el campo de estado ;
Ejemplo de aplicación
Publicar modelo
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Post extends Model { protected $fillable = [ 'name', 'id', 'title', 'slug', 'status', 'type', 'published_at', 'updated_at', ]; }
Agregar una ruta:
Route::get('/posts', 'PostController@index');
Cree un archivo
Resource \ Post para la salida en
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, ]; } }
Y el controlador en sí con una sola acción:
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); } }
El filtrado estándar está organizado por el siguiente 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); }
Con este enfoque, nos enfrentamos al crecimiento del controlador, lo que no es deseable.
Implementando QueryFilter
El significado de tal concepto es usar una clase separada para cada entidad que mapea métodos a cada campo para el filtrado.
Filtrar por solicitud:
GET /posts?title=source&status=publish
Para el filtrado, tendremos la clase
PostFilter y los métodos
title () y
status () .
PostFilter extenderá la clase abstracta
QueryFiler, que es responsable de hacer coincidir los métodos de clase con los parámetros pasados.
Método aplicar ()La clase
QueryFIlter tiene un método
apply () , que es responsable de
invocar filtros que están en la clase secundaria
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()) ); } }
La conclusión es que para cada campo pasado a través de
Solicitud , tenemos un método separado en la clase de filtro secundario (clase PostFilter). Esto nos permite personalizar la lógica para cada campo de filtro.
Clase PostFilterAhora pasemos a crear una clase
PostFilter que extienda
QueryFilter . Como se mencionó anteriormente, esta clase debe contener métodos para cada campo por los cuales tenemos que filtrar. En nuestro caso, los métodos de
título ($ valor) y
estado ($ valor) 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%"); } }); } }
Aquí no veo ninguna razón para un análisis detallado de cada uno de los métodos, consultas bastante estándar. El punto es que ahora tenemos un método separado para cada campo y podemos usar cualquier lógica que necesitemos para formar la solicitud.
Crear scopeFilter ()Ahora tenemos que vincular el modelo y el diseñador de consultas.
public function scopeFilter(Builder $builder, QueryFilter $filter) { $filter->apply($builder); }
Para buscar, necesitamos llamar al método
filter () y pasar una instancia de
QueryFilter , en nuestro caso
PostFilter .
$filteredPosts = Post::filter($postFilter)->get();
Por lo tanto, toda la lógica de filtrado se procesa llamando al método de
filtro ($ postFilter) , salvando al controlador de una lógica innecesaria.
Para facilitar la reutilización, puede colocar el método
scopeFilter en el rasgo y usarlo para cada modelo que necesita ser filtrado.
namespace App\Http\Filters; use Illuminate\Database\Eloquent\Builder; trait Filterable { public function scopeFilter(Builder $builder, QueryFilter $filter) { $filter->apply($builder); } }
En
Publicar agregar:
class Post extends CorcelPost { use Filterable;
Queda por agregar
index () al método del controlador como el parámetro
PostFilter y llamar al método del modelo
filter () .
class PostController extends Controller { public function index(PostFilter $filter) { $posts = Post::filter($filter)->limit(10)->get(); return PostResource::collection($posts); } }
Eso es todo. Movimos toda la lógica de filtrado a la clase apropiada, observando el principio de responsabilidad única (S en el sistema de principios SOLID)
Conclusión
Este enfoque para la implementación de filtros le permite adherirse al controlador delgado del trailer y también facilita la escritura de las pruebas.
Aquí hay un ejemplo usando PHP y Laravel. Pero como dije, este es un concepto y puede funcionar con cualquier lenguaje o marco.
Referencias