Filtros dinámicos con Laravel Eloquent
¿Alguna vez tuvo una aplicación Laravel y necesita filtros dinámicos sobre tu API? Es posible que haya probado GraphQL, sin embargo, podría escribir uno simple y no necesita ninguna integración.
En este artículo vamos a implementar un servicio simple que podría funcionar con el elocuente ORM de Laravel, manejaría las relaciones y también los operadores básicos que más usa.
Filtros dinámicos
La idea
Está desarrollando una API RESTful y desea permitir que sus usuarios filtren los índices con diferentes campos o incluso en las relaciones de su modelo.
Por ejemplo, un modelo de article
simple que tiene tantas tags
y comments
, también un user
que lo posee. Ahora tiene una API que enumera todos los artículos /api/articles
. ¿Cómo podría permitir que el usuario filtrara las etiquetas o los comentarios? ¿Cómo se pueden filtrar todos los artículos de un usuario específico? Además, ¿cómo podrías combinarlos todos juntos?
Agreguemos un filtro a nuestra API para encontrar todos los artículos del usuario con id = 1.
/api/articles?filters[]=user.id=1
¿Parece genial? Ahora quiero los artículos del usuario 1 que tengan una etiqueta específica.
/api/articles?filters[]=user.id=1&filters[]=tags.name=ipsa
En el siguiente paso, configuraremos el proyecto básico y luego comenzaremos a agregarle estos filtros dinámicos.
Configuración
Estaba buscando un proyecto de muestra del mundo real y descubrí que hay una aplicación completa de Laravel que podría usarse. Eche un vistazo a su repositorio de github y recorra el archivo README para configurarlo.
Extiende la idea
Sabemos que hay un parámetro de consulta llamado filters
que contiene la relación / campo, el operador y el valor.
Aquí vamos a implementar los operadores lógicos básicos que son =
, !=
, >
, <
, >=
y <=
. También agregué 2 operadores personalizados ~
y ()
.
~
funcionaría como LIKE
en consultas con %
alrededor.
ex. /api/articles?filters[]=slug~real-world
SQL: SELECT * FROM articles where slug like '%real-world%';
()
funcionaría como IN
en consultas.
e.x. /api/articles?filters[]user.id()1,2
SQL: SELECT * FROM articles where user_id IN(1,2);
Sumérjase en la codificación
El servicio
Primero, necesitamos crear un servicio que pueda tomar la solicitud y crear las consultas apropiadas en un modelo dado para nosotros.
Cree un directorio de Services
en la carpeta de la app
, luego cree un archivo FilterQueryBuilder.php
dentro de él.
# app/Services/FilterQueryBuilder.php<?php namespace App\Services;use Illuminate\Database\Eloquent\Builder;class FilterQueryBuilder { protected $request; public function __construct($request) { $this->request = $request; } }
Ahora agreguemos el código paso a paso.
Necesitamos un método público en esta clase que acepte un constructor elocuente, analice los filtros y le agregue las consultas apropiadas. Ahora agreguemos el método a continuación a nuestro servicio.
# app/Services/FilterQueryBuilder.php ...public function buildQuery($query) { $filters = $this->request->query('filters'); $filters = $this->parseFilters($filters); foreach($filters as $filter) { $query = $this->addFiltersToQuery($query, $filter); } return $query; } ...
Analizando los filtros
Para cada filtro dado en el parámetro de consulta, necesitamos encontrar las relaciones / campo, el operador y el valor, luego podemos agregarlos a nuestra consulta.
Agreguemos un método getOperator
que acepte el valor de la cadena de filtro y nos devuelva el operador.
# app/Services/FilterQueryBuilder.php ... private function getOperator($filter) { $operatorsPattern = '/=|!=|\(\)|>=|<=|~|>|</'; $operator = []; preg_match($operatorsPattern, $filter, $operator); if(count($operator) == 1) { return $operator[0]; } } ...
Gracias a las expresiones regulares, es muy fácil encontrar al operador. Si no está familiarizado con las expresiones regulares, lea más al respecto aquí. También puede practicar estos métodos en línea con el sitio web PHP Live Regex.
Ahora que tenemos el operador, podríamos dividir la cadena de filtro en relaciones / campo y parte de valor. Agreguemos el código para ello.
# app/Services/FilterQueryBuilder.php ...private function getFilterValue($filter, $operator) { $result = explode($operator, $filter); if(count($result) == 2) { return $result[1]; } }private function getFilterRelations($filter, $operator) { $result = explode($operator, $filter); if(count($result) == 2) { return $result[0]; } } ...
Árbol de relaciones
Hasta ahora hemos encontrado las relaciones, el operador y el valor en nuestro filtro. ¿Cómo podríamos agregar esta relación a nuestra consulta?
Por ejemplo, para nuestro primer ejemplo de API, necesitamos crear una matriz anidada como esta.
# /api/articles?filters[]user.id()1,2Array ( [user] => Array ( [field] => id [operator] => () [value] => 1,2 ) )
Para crear el árbol de relaciones, necesitamos las claves de relación, el operador y el valor que ya se han fundado.
# app/Services/FilterQueryBuilder.php ... private function createRelationTree(string $relationKeys, $operator, $value) { $relations = explode('.', $relationKeys); $lastKey = array_key_last($relations); $field = $relations[$lastKey]; unset($relations[$lastKey]); $result = []; if(count($relations) > 0 ) { $result[$relations[count($relations)-1]] = [ 'field' => $field, 'operator' => $operator, 'value' => $value ]; for($i=count($relations)-2; $i>-1; $i--) { $result[$relations[$i]] = $result; unset($result[$relations[$i+1]]); } } else { $result = [ 'field' => $field, 'operator' => $operator, 'value' => $value ]; } return $result; } ...
Este código dividirá toda la relación y la convertirá en una matriz anidada. Si tenemos un filtro como model1.model2.model3.id=100
, la salida sería algo como esto.
Array ( [model1] => Array ( [model2] => Array ( [model3] => Array ( [field] => id [operator] => = [value] => 100 ) ) ))
Ahora agreguemos estos métodos en un método y agreguemos todos los filtros dinámicos.
# app/Services/FilterQueryBuilder.php ... private function parseFilters($filters) { if(empty($filters)) { return []; } $result = []; foreach($filters as $filter) { if(empty($filter)) { continue; } $operator = $this->getOperator($filter); $value = $this->getFilterValue($filter, $operator); $keys = $this->getFilterRelations($filter, $operator); $relationTree = $this->createRelationTree($keys, $operator, $value); array_push($result, $relationTree); } return $result; } ...
Agregar filtro al generador de consultas
Ahora que tenemos todos los filtros en una matriz, debemos agregar estas relaciones a nuestro generador de consultas.
# app/Services/FilterQueryBuilder.php ... private function addFiltersToQuery($query, $filters) { if(count($filters) === 3) { switch($filters['operator']) { case '()': return $query->whereIn($filters['field'], explode(',', $filters['value'])); case '~': return $query->where($filters['field'], 'LIKE', '%' . $filters['value'] . '%'); default: return $query->where($filters['field'], $filters['operator'], $filters['value']); } } $relation = array_key_first($filters); return $query->whereHas($relation, function(Builder $query) use($relation, $filters) { $this->addFiltersToQuery($query, $filters[$relation]); }); } ...
Aquí hay un método recursivo, que iterará sobre la matriz de filtros, si es una relación, entonces usaría whereHas
que le dice al constructor elocuente que se una a las tablas según la definición del modelo.
Además, cuando llegue al final de la matriz, agregará el campo y el valor con el operador dado. Puede ver que también hemos manejado los operadores personalizados.
También puede leer más sobre el generador de consultas Eloquent y laravel.
Proveedor de servicio
Los proveedores de servicios son el lugar central de todas las aplicaciones de arranque de Laravel. Su propia aplicación, así como todos los servicios centrales de Laravel, se arrancan a través de proveedores de servicios.
Puede leer más sobre la idea del proveedor de servicios aquí.
El proveedor de servicios lo ayudará a inyectar su servicio con el contenedor de servicios laravel en su aplicación. Se encargaría de crear la instancia y pasarla a tus clases y métodos.
Debe ejecutar el siguiente comando para crear un proveedor de servicios llamado FilterServiceProvider
.
$ php artisan make:provider FilterServiceProvider
Entonces eche un vistazo al archivo a continuación.
# app/Providers/FilterServiceProvider.php<?phpnamespace App\Providers;use Illuminate\Support\ServiceProvider;class FilterServiceProvider extends ServiceProvider { /** * Bootstrap the application services. * * @return void */ public function boot() { // }/** * Register the application services. * * @return void */ public function register() { // } }
Ahora digamos cómo estamos creando una instancia de nuestro servicio FilterQueryBuilder
dentro del método de register
.
# app/Providers/FilterServiceProvider.php ... public function register() { $this->app->bind(\App\Services\FilterQueryBuilder::class, function () { $request = app(\Illuminate\Http\Request::class); return new FilterQueryBuilder($request); }); } ...
Entonces, siempre que en la aplicación, escriba insinuado la clase FilterQueryBuilder
, pasará el objeto \Illuminate\Http\Request
al constructor y le dará una instancia.
Después de haber agregado su proveedor de servicios, debe registrarlo para que laravel lo entienda. Ahora abra el archivo config/app.php
y, debajo de la clave de providers
, agregue también su proveedor.
# config/app.php ... 'providers' => [ ... App\Providers\FilterServiceProvider::class, ...]...
¿Cómo usarlo ahora?
Hasta ahora tan bueno. Ahora puede echar un vistazo al código API del índice del artículo en app/Http/Controllers/Api/ArticleController.php
# app/Http/Controllers/Api/ArticleController.php ... public function index(ArticleFilter $filter) { $articles = new Paginate(Article::loadRelations()->filter($filter)); return $this->respondWithPagination($articles); }...
Sin embargo, han creado una clase ArticleFilter
, pero si echas un vistazo al código, solo manejaría alguna relación determinada. Lo reemplazaremos con nuestro FilterQueryBuilder
que aceptaría filtros dinámicos en nuestro modelo basado en la definición del modelo. Entonces, en el futuro, solo necesitamos actualizar nuestro modelo y los filtros también funcionarán en ellos.
# app/Http/Controllers/Api/ArticleController.php... public function index(\App\Services\FilterQueryBuilder $filters) { $articles = $filters->buildQuery(Article::loadRelations()); $result = new Paginate($articles); return $this->respondWithPagination($result); } ...
¡Tenga cuidado!
Debe saber que esto fue solo una práctica y no está optimizado para aplicaciones a gran escala y también debe manejar muchas otras cosas. No dude en comunicarse y ampliarlo si quiere.