## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require czim/laravel-filter
Publish the config file (if needed):
php artisan vendor:publish --provider="Czim\LaravelFilter\LaravelFilterServiceProvider"
Basic Usage:
Define a filter class (e.g., ProductFilter) extending Czim\LaravelFilter\Filter:
use Czim\LaravelFilter\Filter;
class ProductFilter extends Filter
{
protected $query;
public function __construct($query)
{
$this->query = $query;
}
public function apply()
{
if ($this->has('category')) {
$this->query->where('category_id', $this->get('category'));
}
if ($this->has('price_min')) {
$this->query->where('price', '>=', $this->get('price_min'));
}
return $this;
}
}
First Use Case: Apply the filter in a controller:
use App\Filters\ProductFilter;
public function index(Request $request)
{
$query = Product::query();
$filter = new ProductFilter($query);
$filter->apply($request->all());
return ProductResource::collection($filter->getQuery()->get());
}
Separate Filters by Domain:
Create dedicated filter classes for different models (e.g., UserFilter, OrderFilter).
Example:
class UserFilter extends Filter
{
public function apply()
{
if ($this->has('role')) {
$this->query->where('role', $this->get('role'));
}
if ($this->has('active')) {
$this->query->where('active', $this->get('active'));
}
return $this;
}
}
Reusable Filter Logic: Extend base filters for shared functionality. Example:
class BaseProductFilter extends Filter
{
protected function applyPriceRange()
{
if ($this->has('price_min') || $this->has('price_max')) {
$this->query->whereBetween('price', [
$this->get('price_min', 0),
$this->get('price_max', PHP_INT_MAX),
]);
}
}
}
class ProductFilter extends BaseProductFilter
{
public function apply()
{
$this->applyPriceRange();
// Additional filters...
}
}
Dynamic Filter Application: Use middleware or a base controller to apply filters automatically:
class FilterableController extends Controller
{
protected function applyFilters($query, $filterClass, $request)
{
$filter = new $filterClass($query);
return $filter->apply($request->all())->getQuery();
}
}
Countable Filters:
Extend Czim\LaravelFilter\CountableFilter for faceted navigation:
class ProductFilter extends CountableFilter
{
public function apply()
{
// Apply filters as usual
return $this;
}
public function getCounts()
{
return [
'categories' => $this->getCategoryCounts(),
'brands' => $this->getBrandCounts(),
];
}
protected function getCategoryCounts()
{
return Product::select('category_id', \DB::raw('COUNT(*) as count'))
->where($this->query->getQuery()->getBindings(), $this->query->getQuery()->getValues())
->groupBy('category_id')
->pluck('count', 'category_id');
}
}
Integration with API Resources: Combine filters with Laravel API Resources for consistent responses:
public function index(Request $request)
{
$query = Product::query();
$filter = new ProductFilter($query);
$filter->apply($request->all());
return new ProductCollection($filter->getQuery()->paginate());
}
Form Request Integration:
Validate filter inputs in a FormRequest:
class ProductFilterRequest extends FormRequest
{
public function rules()
{
return [
'category' => 'sometimes|integer|exists:products,category_id',
'price_min' => 'sometimes|numeric|min:0',
'price_max' => 'sometimes|numeric|min:0',
];
}
}
Query Binding Issues:
$this->query->getQuery()->getBindings() and $this->query->getQuery()->getValues() to preserve bindings:
$baseQuery = $this->query->getQuery();
$countQuery = Product::select(\DB::raw('COUNT(*)'))
->where($baseQuery->getBindings(), $baseQuery->getValues());
Case Sensitivity in Filters:
whereRaw or modify the query:
if ($this->has('name')) {
$this->query->whereRaw('LOWER(name) LIKE LOWER(?)', ["%{$this->get('name')}%"]);
}
Performance with Large Datasets:
protected function getCategoryCounts()
{
return Cache::remember("product_category_counts_{$this->getCacheKey()}", now()->addHours(1), function () {
return Product::select('category_id', \DB::raw('COUNT(*) as count'))
->where($this->query->getQuery()->getBindings(), $this->query->getQuery()->getValues())
->groupBy('category_id')
->pluck('count', 'category_id');
});
}
Overwriting Query Bindings:
where methods instead of whereRaw when possible, or manually manage bindings.Filter Order Matters:
where clauses can affect performance and results.if ($this->has('active')) {
$this->query->where('active', $this->get('active')); // Filter first
}
if ($this->has('name')) {
$this->query->where('name', 'like', "%{$this->get('name')}%"); // Then less restrictive
}
Log Filtered Queries: Use Laravel's query logging to debug:
\DB::enableQueryLog();
$filter->apply($request->all());
dd(\DB::getQueryLog());
Inspect Request Data: Ensure filter parameters are being passed correctly:
dd($request->all()); // Check incoming data
Test Filters in Isolation: Write unit tests for filter logic:
public function test_product_filter_applies_correctly()
{
$query = Product::query();
$filter = new ProductFilter($query);
$filter->apply(['category' => 1, 'price_min' => 10]);
$expected = Product::query()->where('category_id', 1)->where('price', '>=', 10);
$this->assertEquals($expected->toSql(), $filter->getQuery()->toSql());
}
Custom Filter Classes:
config/laravel-filter.php:
'filter_class' => \App\Filters\CustomFilter::class,
Modular Filter Registration:
$this->app->bind(\App\Filters\ProductFilter::class, function ($app) {
return new \App\Filters\ProductFilter(Product::query());
});
Handling Empty Filters:
if ($this->has('name') && !is_null($this->get('name'))) {
$this->query->where('name', 'like', "%{$this->get('name')}%");
}
contains, starts_with):
class ProductFilter extends Filter
{
public function apply()
{
if ($this->has('name_contains')) {
$this->query->where('name', 'like',
How can I help you explore Laravel packages today?