Filterable en Laravel: Filtrado Avanzado de Modelos Eloquent
Introducción
Uno de los desafíos más comunes al desarrollar aplicaciones Laravel es implementar sistemas de filtrado flexible y escalable para tus modelos Eloquent. Cuando necesitas filtrar por múltiples campos, relaciones anidadas y valores JSON, rápidamente el código se vuelve complejo y difícil de mantener.
Filterable es un paquete Laravel que resuelve este problema proporcionando un sistema declarativo y poderoso para filtrar modelos Eloquent. En este artículo, exploraremos cómo implementarlo en tus proyectos y cómo puede transformar la forma en que manejas consultas filtradas.
¿Qué es Filterable?
Filterable es un paquete Laravel que ofrece múltiples engines para realizar filtrado avanzado de modelos Eloquent. Sus características principales incluyen:
- Filtrado anidado: Soporte para JSON y relaciones complejas
- Múltiples engines: Diferentes estrategias de filtrado según necesites
- Sintaxis declarativa: Define filtros de forma clara y legible
- Escalabilidad: Optimizado para aplicaciones grandes
- Integración fluida: Funciona nativamente con Eloquent
Instalación de Filterable
El proceso de instalación es simple usando Composer:
composer require spinoperator/filterable
Una vez instalado, el paquete está listo para usar sin necesidad de configuración adicional en la mayoría de los casos.
Conceptos Fundamentales
Traits y Setup Inicial
Para comenzar a usar Filterable, necesitas agregar el trait Filterable a tus modelos:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spinoperator\Filterable\Traits\Filterable;
class Product extends Model
{
use Filterable;
protected $fillable = ['name', 'description', 'price', 'status'];
}
Definición de Filtros
Los filtros se definen en una clase separada que implementa la interfaz de filtros:
<?php
namespace App\Filters;
use Spinoperator\Filterable\Filter;
class ProductFilter extends Filter
{
protected $filters = [
'name' => 'like',
'price' => 'equals',
'status' => 'in',
'description' => 'like',
];
}
Uso Práctico: Ejemplo Completo
Veamos un ejemplo real de cómo implementar filtrado en una aplicación de comercio electrónico.
Modelo con Filtros Avanzados
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spinoperator\Filterable\Traits\Filterable;
class Product extends Model
{
use Filterable;
protected $fillable = [
'name',
'description',
'price',
'status',
'category_id',
'attributes',
];
protected $casts = [
'attributes' => 'json',
'price' => 'decimal:2',
];
public function category()
{
return $this->belongsTo(Category::class);
}
public function reviews()
{
return $this->hasMany(Review::class);
}
}
Definición de Filtros Complejos
<?php
namespace App\Filters;
use Spinoperator\Filterable\Filter;
use Illuminate\Database\Eloquent\Builder;
class ProductFilter extends Filter
{
protected $filters = [
'name' => 'like',
'price_min' => 'min_price',
'price_max' => 'max_price',
'status' => 'equals',
'category_id' => 'equals',
'rating' => 'min_rating',
'color' => 'json_filter',
];
// Filtro personalizado para rango de precio
public function minPrice(Builder $query, $value)
{
return $query->where('price', '>=', $value);
}
public function maxPrice(Builder $query, $value)
{
return $query->where('price', '<=', $value);
}
// Filtro para calificación mínima
public function minRating(Builder $query, $value)
{
return $query->withAvg('reviews', 'rating')
->having('reviews_avg_rating', '>=', $value);
}
// Filtro para JSON anidado
public function jsonFilter(Builder $query, $value)
{
return $query->where('attributes->color', $value);
}
}
Uso en Controladores
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Filters\ProductFilter;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index(Request $request)
{
// Crear una instancia del filtro
$filter = new ProductFilter($request->all());
// Aplicar filtros al query
$products = Product::filter($filter)
->with('category', 'reviews')
->paginate(15);
return response()->json($products);
}
public function search(Request $request)
{
// Filtrado simple con validación
$validated = $request->validate([
'name' => 'nullable|string|max:255',
'price_min' => 'nullable|numeric|min:0',
'price_max' => 'nullable|numeric|min:0',
'status' => 'nullable|in:active,inactive,draft',
'category_id' => 'nullable|exists:categories,id',
'rating' => 'nullable|numeric|min:0|max:5',
]);
$filter = new ProductFilter($validated);
$products = Product::filter($filter)
->select('id', 'name', 'price', 'status', 'category_id')
->get();
return view('products.search', ['products' => $products]);
}
}
Filtrado con Relaciones
Filtros en Relaciones Anidadas
<?php
namespace App\Filters;
use Spinoperator\Filterable\Filter;
use Illuminate\Database\Eloquent\Builder;
class ProductFilter extends Filter
{
protected $filters = [
'name' => 'like',
'category_name' => 'filter_category',
'author_name' => 'filter_author',
];
// Filtrar por nombre de categoría
public function filterCategory(Builder $query, $value)
{
return $query->whereHas('category', function ($q) use ($value) {
$q->where('name', 'like', "%{$value}%");
});
}
// Filtrar por autor a través de categoría
public function filterAuthor(Builder $query, $value)
{
return $query->whereHas('category.author', function ($q) use ($value) {
$q->where('name', 'like', "%{$value}%");
});
}
}
Uso en Request
// Request GET a /api/products?name=Laptop&category_name=Electronics&author_name=TechBrand
$filter = new ProductFilter($request->all());
$products = Product::filter($filter)->get();
Filtrado de JSON Avanzado
Trabajar con Datos JSON Anidados
<?php
namespace App\Filters;
use Spinoperator\Filterable\Filter;
use Illuminate\Database\Eloquent\Builder;
class ProductFilter extends Filter
{
protected $filters = [
'color' => 'json_color',
'size' => 'json_size',
'specs' => 'json_specs',
];
public function jsonColor(Builder $query, $value)
{
return $query->where('attributes->color', $value);
}
public function jsonSize(Builder $query, $value)
{
return $query->whereJsonContains('attributes->sizes', $value);
}
public function jsonSpecs(Builder $query, $value)
{
// Filtrar múltiples especificaciones
return $query->where('attributes->specifications->weight', '>', $value);
}
}
Optimización y Mejores Prácticas
Evitar N+1 Queries
<?php
public function index(Request $request)
{
$filter = new ProductFilter($request->all());
$products = Product::filter($filter)
->with([
'category' => function ($query) {
$query->select('id', 'name');
},
'reviews' => function ($query) {
$query->select('id', 'product_id', 'rating')->limit(5);
}
])
->select('id', 'name', 'price', 'category_id')
->paginate(15);
return response()->json($products);
}
Caché de Filtros Frecuentes
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Filters\ProductFilter;
use Illuminate\Support\Facades\Cache;
public function index(Request $request)
{
$cacheKey = 'products:filter:' . md5(json_encode($request->all()));
$products = Cache::remember($cacheKey, 3600, function () use ($request) {
$filter = new ProductFilter($request->all());
return Product::filter($filter)
->with('category')
->paginate(15);
});
return response()->json($products);
}
Validación de Filtros
<?php
public function index(Request $request)
{
$validated = $request->validate([
'name' => 'nullable|string|max:100',
'price_min' => 'nullable|numeric|min:0|max:1000000',
'price_max' => 'nullable|numeric|min:0|max:1000000',
'status' => 'nullable|in:active,inactive,draft',
'category_id' => 'nullable|integer|exists:categories,id',
'rating' => 'nullable|numeric|between:0,5',
'page' => 'nullable|integer|min:1',
'per_page' => 'nullable|integer|between:1,100',
]);
$filter = new ProductFilter($validated);
$products = Product::filter($filter)
->paginate($validated['per_page'] ?? 15);
return response()->json($products);
}
Comparación: Antes vs Después
Sin Filterable
// Código tedioso y repetitivo
$query = Product::query();
if ($request->has('name')) {
$query->where('name', 'like', "%{$request->name}%");
}
if ($request->has('price_min')) {
$query->where('price', '>=', $request->price_min);
}
if ($request->has('price_max')) {
$query->where('price', '<=', $request->price_max);
}
if ($request->has('status')) {
$query->where('status', $request->status);
}
if ($request->has('rating')) {
$query->withAvg('reviews', 'rating')
->having('reviews_avg_rating', '>=', $request->rating);
}
$products = $query->paginate(15);
Con Filterable
// Código limpio y mantenible
$filter = new ProductFilter($request->all());
$products = Product::filter($filter)->paginate(15);
Casos de Uso Reales
E-commerce
// Búsqueda avanzada de productos
$filter = new ProductFilter([
'name' => 'laptop',
'price_min' => 500,
'price_max' => 2000,
'category_id' => 1,
'rating' => 4,
'color' => 'black'
]);
$products = Product::filter($filter)->with('reviews')->get();
Sistema de Gestión de Usuarios
class UserFilter extends Filter
{
protected $filters = [
'email' => 'like',
'role' => 'equals',
'status' => 'equals',
'created_after' => 'date_min',
'created_before' => 'date_max',
];
}
API REST con Filtrado
// GET /api/users?role=admin&status=active&created_after=2024-01-01
$filter = new UserFilter($request->all());
$users = User::filter($filter)->paginate(50);
return UserResource::collection($users);
Conclusión
Filterable es un paquete poderoso que simplifica significativamente la implementación de sistemas de filtrado complejos en Laravel. Al adoptar este enfoque declarativo, tu código se vuelve más limpio, mantenible y escalable.
Las ventajas clave incluyen:
- Código más legible: Los filtros se definen de forma clara y centralizada
- Reutilización: Puedes reutilizar los mismos filtros en múltiples controladores
- Mantenimiento: Cambios en la lógica de filtrado se hacen en un único lugar
- Flexibilidad: Soporta desde filtros simples hasta complejos con relaciones y JSON
- Performance: Te permite optimizar queries y agregar caché fácilmente
Si trabajas frecuentemente con búsquedas y filtros en tus aplicaciones Laravel, Filterable es definitivamente una inversión valiosa que te ahorrará horas de desarrollo y dolor de cabeza.
Puntos Clave
- Filterable simplifica el código de filtrado mediante un sistema declarativo y reutilizable
- Soporta múltiples tipos de filtrado: campos simples, JSON anidado, relaciones complejas
- Define filtros personalizados extendiendo la clase Filter con métodos específicos
- Integración natural con Eloquent usando el trait Filterable en tus modelos
- Mejora el performance permitiendo eager loading, caché y validación de filtros
- Mantiene el código DRY centralizando la lógica de filtrado en clases dedicadas
- Escalable para aplicaciones grandes con soporte para relaciones anidadas y datos complejos