devaction-labs/livewire-filterable
Install the package:
composer require devaction-labs/livewire-filterable
Ensure your project meets requirements: PHP 8.5+, Laravel 11/12, Livewire 4.
Add traits to your Eloquent model:
use DevactionLabs\LivewireFilterable\Traits\Filterable;
use DevactionLabs\LivewireFilterable\Concerns\HasCustomPagination;
class Customer extends Model
{
use Filterable, HasCustomPagination;
}
Create a Livewire component:
use DevactionLabs\LivewireFilterable\Concerns\LivewireFilterable;
use DevactionLabs\LivewireFilterable\Filter;
class CustomerList extends Component
{
use LivewireFilterable;
public string $name = '';
public function render()
{
return Customer::query()
->filterable([Filter::ilike('name')])
->customPaginate()
->toLivewireResponse();
}
}
Bind inputs to public properties in Blade:
<input wire:model.live="name" type="text" placeholder="Search...">
Build a reactive search interface for a Product table with:
name, description)class ProductSearch extends Component
{
use LivewireFilterable;
public string $search = '';
public array $priceRange = [];
public ?int $categoryId = null;
public function render()
{
return Product::query()
->filterable([
Filter::ilike('name')->debounce(500),
Filter::between('price', 'priceRange'),
Filter::exact('category_id', 'categoryId')
])
->customPaginate()
->toLivewireResponse();
}
}
Property Binding: Public Livewire properties automatically bind to filters when named identically to database columns.
// Automatically filters `email` column
public string $email = '';
Filter Composition:
Chain filters using the fluent Filter class:
Filter::ilike('name')
->debounce(500)
->caseInsensitive()
Pagination Integration:
->customPaginate('cursor', $this->perPage)
// Filter products by related category slug
Filter::relationship('category', 'slug', '=', 'categorySlug')
->with(['category' => function($query) {
$query->select('id', 'name');
}])
// Filter orders from today
public string $today = '';
Filter::exact('created_at', 'today')
->castDate()
->startOfDay()
->endOfDay()
// Conditionally apply filters
if ($this->showActiveOnly) {
$filters[] = Filter::exact('is_active', true);
}
use Livewire\Attributes\Url;
class Search extends Component
{
#[Url] public string $query = '';
#[Url(as: 'min_price')] public ?float $minPrice = null;
}
Form Validation:
protected $rules = [
'name' => 'sometimes|string|max:255',
'priceRange' => 'sometimes|array|min:2',
];
Debouncing:
wire:model.live.debounce.500msFilter::like('name')->debounce(500)Reset Filters:
public function resetFilters()
{
$this->reset(['name', 'email', 'categoryId']);
}
Custom Property Mapping:
Filter::ilike('full_name', 'searchName')
Performance Optimization:
// Use cursor pagination for large datasets
->customPaginate('cursor', 50)
Property Naming Mismatch:
public string $userName won't filter username column.Filter::exact('username', 'userName').Debounce Conflicts:
Relationship Loading:
->with() can cause N+1 queries.->with() for relationship filters.Date Handling:
castDate() without timezone awareness may cause off-by-day issues.Filter::exact('created_at')
->castDate()
->setTimezone('America/New_York')
Full-Text Search:
tsvector requires proper indexing.Query Logging: Enable Laravel query logging to verify filters:
\DB::enableQueryLog();
$results = Product::query()->filterable([...])->get();
\Log::info(\DB::getQueryLog());
Filter Validation:
Use Filter::validate() to check filter syntax:
try {
Filter::ilike('name')->validate();
} catch (\InvalidArgumentException $e) {
// Handle error
}
Performance Profiling: Compare execution times:
$start = microtime(true);
$results = Product::query()->filterable([...])->get();
$time = microtime(true) - $start;
\Log::info("Query took {$time}s");
Database-Specific Features:
ilike() behaves differently across databases:
ILIKELOWER()LIKE (case-insensitive by default)JSON Field Handling:
Filter::json('metadata', 'specs.weight', '>', 'min_weight')
->setDatabaseDriver('mysql') // or 'pgsql'
Pagination Types:
cursor pagination requires unique, sortable columns.simple pagination is fastest but lacks total count.Custom Filter Types:
Extend the Filter class to add domain-specific filters:
class CustomFilter extends Filter
{
public static function customLogic(string $column): self
{
return new static($column, 'custom_logic');
}
protected function apply(Builder $query, $value): Builder
{
return $query->where($this->column, 'custom_logic_value');
}
}
Filter Events: Listen for filter application:
Filter::listen(function (Filter $filter, Builder $query) {
\Log::debug("Applying filter: {$filter->getColumn()}");
});
Model-Specific Filtering:
Override getFilterableAttributes() in your model:
public function getFilterableAttributes(): array
{
return ['name', 'email', 'created_at'];
}
Custom Pagination Logic:
Extend HasCustomPagination trait:
class CustomPagination
{
public function apply(Builder $query, string $type, int $perPage): Builder
{
// Custom logic
}
}
Use #[NoDiscard] Attributes:
Leverage PHP 8.5's #[\NoDiscard] for fluent method chains:
Filter::ilike('name')->debounce(500)->caseInsensitive();
Pipe Operator for Chaining:
$filters = collect([])
->pipe(fn($c) => $c->push(Filter::ilike('name')))
->pipe(fn($c) => $c->push(Filter::exact('status')));
Type-Safe Filtering: Use PHP 8.5's typed properties:
public string $search = '';
public int $minPrice = 0;
public ?DateTimeInterface $startDate = null;
Bulk Filter Operations:
$filters = collect([
'name' => Filter::ilike('name'),
'price' => Filter::gt('price'),
How can I help you explore Laravel packages today?