Filtros dinámicos con Laravel Eloquent

filtros dinámicos

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 tagscomments , 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 = , != , > , < , >=<=. También agregué 2 operadores personalizados ~().

~ 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.

Recent Post