QueryFilter: el concepto de filtrado de modelos

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 { /** * The attributes that are mass assignable. * * @var array */ 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 { /** * 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, ]; } } 

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 { /** * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection */ public function index() { $posts = Post::limit(10)->get(); return PostResource::collection($posts); } } 

El filtrado estándar está organizado por el siguiente 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); } 

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

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 PostFilter

Ahora 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 { /** * @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%"); } }); } } 

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.

 /** * @param Builder $builder * @param QueryFilter $filter */ 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 { /** * @param Builder $builder * @param QueryFilter $filter */ 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 { /** * @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); } } 

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


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


All Articles