Language:

Buscar

Cómo implementar filtros correctamente en Laravel usando clases Filter (Query Filters Pattern)

  • Share this:
Cómo implementar filtros correctamente en Laravel usando clases Filter (Query Filters Pattern)

Si trabajas con Laravel en proyectos medianos o grandes, tarde o temprano te encontrarás con endpoints llenos de filtros: por estado, fecha, usuario, rango de precios, búsqueda por texto, etc. Y si no tienes cuidado, terminas con controladores gigantes, lógica duplicada y queries difíciles de mantener.

Hoy quiero enseñarte un enfoque que llevo años usando en producción y que funciona muy bien: clases de filtros que extienden de una clase base QueryFilter.

Te lo explico como si estuviéramos revisando código juntos.


El problema típico


Seguro has visto algo así:

public function index(Request $request)
{
    $query = Order::query();

    if ($request->status) {
        $query->where('status', $request->status);
    }

    if ($request->user_id) {
        $query->where('user_id', $request->user_id);
    }

    if ($request->from_date) {
        $query->whereDate('created_at', '>=', $request->from_date);
    }

    if ($request->to_date) {
        $query->whereDate('created_at', '<=', $request->to_date);
    }

    return $query->paginate();
}

 

Funciona… pero:

  • El controlador crece demasiado
  • No es reutilizable
  • No es testeable fácilmente
  • Rompe SRP (Single Responsibility Principle)
  • Si agregas más filtros, se vuelve inmanejable 

Query Filters Pattern es la solución


La idea es separar cada filtro en su propia clase (o método) y aplicarlos dinámicamente según los parámetros recibidos.

Ventajas:

  • Código limpio
  • Reutilizable
  • Escalable
  • Testeable
  • Muy expresivo
  • Ideal para APIs

Vamos a crear:

  • Una clase abstracta QueryFilter
  • Una clase concreta por modelo (ej: OrderFilters)
  • Métodos que representan filtros
  • Aplicación automática según parámetros del request

Paso 1 — Crear la clase base QueryFilter


Esta clase se encargará de:

  • Leer los parámetros del request
  • Detectar qué filtros aplicar
  • Ejecutarlos sobre el query builder
namespace App\Filters;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;

abstract class QueryFilter
{
    protected Builder $builder;
    protected Request $request;

    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    public function apply(Builder $builder): Builder
    {
        $this->builder = $builder;

        foreach ($this->filters() as $name => $value) {
            if (method_exists($this, $name)) {
                $this->$name($value);
            }
        }

        return $this->builder;
    }

    protected function filters(): array
    {
        return $this->request->all();
    }
}

 

Lo importante aquí es:

  • Itera todos los parámetros del request
  • Si existe un método con ese nombre, lo ejecuta
  • Cada método modifica el builder

Paso 2 — Crear filtros específicos del modelo


Ejemplo: OrderFilters
 

php artisan make:class Filters/OrderFilters
namespace App\Filters;

class OrderFilters extends QueryFilter
{
    public function status(string $status): void
    {
        $this->builder->where('status', $status);
    }

    public function user_id(int $userId): void
    {
        $this->builder->where('user_id', $userId);
    }

    public function from_date(string $date): void
    {
        $this->builder->whereDate('created_at', '>=', $date);
    }

    public function to_date(string $date): void
    {
        $this->builder->whereDate('created_at', '<=', $date);
    }
}


Cada método representa un filtro posible.

Si el request contiene:

/api/orders?status=paid&user_id=5

Se ejecutarán automáticamente:

  • status('paid')
  • user_id(5)

Paso 3 — Integrarlo en el modelo (Scope)


Para hacerlo elegante, podemos usar un Local Scope.

// App\Models\Order.php

use App\Filters\OrderFilters;

public function scopeFilter($query, OrderFilters $filters)
{
    return $filters->apply($query);
}

 


Paso 4 — Usarlo en el controlador


Ahora el controlador queda extremadamente limpio:

public function index(OrderFilters $filters)
{
    $orders = Order::query()
        ->filter($filters)
        ->latest()
        ->paginate();

    return OrderResource::collection($orders);
}

 


Ejemplo real de uso

Petición:

GET /api/orders?status=paid&from_date=2026-01-01


Query Resultante

SELECT * FROM orders
WHERE status = 'paid'
AND created_at >= '2026-01-01'
ORDER BY created_at DESC


Sin condiciones manuales ni código duplicado.


Paso 5 — Filtros más avanzados


Búsqueda por texto

public function search(string $term): void
{
    $this->builder->where(function ($q) use ($term) {
        $q->where('title', 'like', "%{$term}%")
          ->orWhere('description', 'like', "%{$term}%");
    });
}

 


Rangos

/api/orders?min_total=100&max_total=500
public function min_total($value): void
{
    $this->builder->where('total', '>=', $value);
}

public function max_total($value): void
{
    $this->builder->where('total', '<=', $value);
}

 


Filtros por relaciones

public function customer(string $name): void
{
    $this->builder->whereHas('customer', function ($q) use ($name) {
        $q->where('name', 'like', "%{$name}%");
    });
}

 


Tips importantes de producción

Valida los parámetros antes

Usa FormRequest para evitar datos inválidos.
 


Evita filtrar todo indiscriminadamente

Puedes restringir qué parámetros son válidos:

protected array $allowedFilters = [
    'status',
    'user_id',
    'from_date',
    'to_date',
];


Y luego:

protected function filters(): array
{
    return $this->request->only($this->allowedFilters);
}

Hazlos reutilizables

Puedes tener filtros compartidos:

  • DateRangeFilter
  • SortableFilter
  • SearchFilter
     

¿Cuándo usar este patrón?

Úsalo cuando:

  • Tu API tiene muchos filtros
  • Quieres mantener controladores limpios
  • El proyecto va a crecer
  • Trabajas en equipo
  • Quieres código mantenible

No es necesario para CRUD simples.


El patrón de Query Filters es una de esas cosas que, una vez lo adoptas, ya no quieres volver atrás.

Te permite:

  • Escribir código elegante
  • Separar responsabilidades
  • Escalar sin dolor
  • Mantener consistencia en toda la API

Si tu proyecto Laravel empieza a tener lógica de filtrado compleja, este enfoque te va a ahorrar muchísimo tiempo (y dolores de cabeza).

 

Etiquetas:
Carlos Santiago

Carlos Santiago

Laravel Developer