Fitur filter dan pencarian data adalah kebutuhan yang sangat umum dalam aplikasi web โ dari filter produk di e-commerce hingga pencarian laporan di sistem manajemen. Di Laravel, ada beberapa cara untuk mengimplementasikannya, mulai dari yang paling sederhana hingga yang paling terstruktur. Artikel ini akan mengajarkan kamu pola filter data yang bersih, mudah diperluas, dan tidak mengotori controller.
Pendekatan Sederhana: Filter Langsung di Controller
Cara paling dasar adalah menggunakan kondisional di query builder. Cocok untuk filter yang tidak terlalu banyak:
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index(Request $request)
{
$query = Product::query();
// Filter berdasarkan nama (pencarian)
if ($request->filled('search')) {
$query->where('name', 'like', '%' . $request->search . '%');
}
// Filter berdasarkan kategori
if ($request->filled('category_id')) {
$query->where('category_id', $request->category_id);
}
// Filter range harga
if ($request->filled('min_price')) {
$query->where('price', '>=', $request->min_price);
}
if ($request->filled('max_price')) {
$query->where('price', '<=', $request->max_price);
}
// Sorting
$sortBy = $request->get('sort_by', 'created_at');
$sortDir = $request->get('sort_dir', 'desc');
$allowedSorts = ['name', 'price', 'created_at'];
if (in_array($sortBy, $allowedSorts)) {
$query->orderBy($sortBy, $sortDir === 'asc' ? 'asc' : 'desc');
}
$products = $query->paginate(15)->withQueryString();
return view('products.index', compact('products'));
}
}
Membuat Form Filter di Blade
<form method="GET" action="{{ route('products.index') }}">
<input type="text" name="search" value="{{ request('search') }}" placeholder="Cari produk...">
<select name="category_id">
<option value="">Semua Kategori</option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ request('category_id') == $cat->id ? 'selected' : '' }}>
{{ $cat->name }}
</option>
@endforeach
</select>
<input type="number" name="min_price" value="{{ request('min_price') }}" placeholder="Harga min">
<input type="number" name="max_price" value="{{ request('max_price') }}" placeholder="Harga maks">
<button type="submit">Filter</button>
<a href="{{ route('products.index') }}">Reset</a>
</form>
Gunakan withQueryString() pada paginator agar parameter filter tetap ada saat berpindah halaman.
Pendekatan Lebih Bersih: Local Scope di Model
Untuk filter yang reusable, gunakan Eloquent local scope:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
public function scopeSearch(Builder $query, ?string $search): Builder
{
return $query->when($search, function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%");
});
}
public function scopeCategory(Builder $query, ?int $categoryId): Builder
{
return $query->when($categoryId, fn($q) => $q->where('category_id', $categoryId));
}
public function scopePriceRange(Builder $query, ?float $min, ?float $max): Builder
{
return $query
->when($min, fn($q) => $q->where('price', '>=', $min))
->when($max, fn($q) => $q->where('price', '<=', $max));
}
}
Penggunaan di controller menjadi sangat bersih:
$products = Product::query()
->search($request->search)
->category($request->category_id)
->priceRange($request->min_price, $request->max_price)
->latest()
->paginate(15)
->withQueryString();
Pendekatan Profesional: Filter Class Terpisah
Untuk aplikasi berskala besar dengan banyak filter, buat kelas filter tersendiri agar controller tetap tipis:
<?php
// app/Filters/ProductFilter.php
namespace App\Filters;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
class ProductFilter
{
public function __construct(protected Request $request) {}
public function apply(Builder $query): Builder
{
if ($this->request->filled('search')) {
$search = $this->request->search;
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('sku', $search);
});
}
if ($this->request->filled('category_id')) {
$query->where('category_id', $this->request->category_id);
}
if ($this->request->filled('min_price')) {
$query->where('price', '>=', $this->request->min_price);
}
if ($this->request->filled('max_price')) {
$query->where('price', '<=', $this->request->max_price);
}
if ($this->request->filled('in_stock')) {
$query->where('stock', '>', 0);
}
return $query;
}
}
Gunakan di controller dengan inject:
use App\Filters\ProductFilter;
public function index(Request $request, ProductFilter $filter)
{
$products = $filter->apply(Product::query())
->latest()
->paginate(15)
->withQueryString();
return view('products.index', compact('products'));
}
Menjaga State Filter di View
Pastikan nilai filter tetap terisi setelah form disubmit dengan menggunakan helper request():
<input type="text"
name="search"
value="{{ request('search') }}"
placeholder="Cari nama atau SKU...">
Kesimpulan
Pilih pendekatan yang sesuai dengan kompleksitas proyekmu. Untuk filter sederhana, langsung di controller sudah cukup. Untuk yang lebih kompleks dan reusable, gunakan local scope di model. Jika filter sudah sangat banyak dan perlu digunakan di banyak tempat, pisahkan ke kelas Filter tersendiri. Dengan pola yang tepat, fitur filter kamu akan mudah diperluas dan dipelihara tanpa mengotori kode di mana-mana.