| Install | |
|---|---|
composer require devaction-labs/livewire-filterable |
|
| Latest Version: | v1.0.0.0 |
| PHP: | ^8.5.0 |
A Laravel Livewire package for elegant, reactive model filtering with zero boilerplate. No more manual when() clauses!
when() clauses neededFilter::like('name')#[\NoDiscard] attribute on all fluent methods (30+ methods)|> for data transformation pipelines#[Scope] attribute for cleaner scopeswhereAny, whereAll, whereNonestartOfDay/endOfDaycomposer require devaction-labs/livewire-filterable
Requirements:
Note: PHP 8.5 is currently available on Linux. macOS and Windows support will be available when PHP 8.5 stable builds are released for those platforms.
namespace App\Models;
use DevactionLabs\LivewireFilterable\Traits\Filterable;
use DevactionLabs\LivewireFilterable\Concerns\HasCustomPagination;
use Illuminate\Database\Eloquent\Model;
class Customer extends Model
{
use Filterable;
use HasCustomPagination;
}
namespace App\Livewire;
use App\Models\Customer;
use DevactionLabs\LivewireFilterable\Concerns\LivewireFilterable;
use DevactionLabs\LivewireFilterable\Filter;
use Livewire\Component;
class CustomerList extends Component
{
use LivewireFilterable;
// Public properties = automatic filters!
public string $name = '';
public string $legal_name = '';
public string $email = '';
public string $tax_id = '';
public string $tax_id_type = '';
public int $tenant_id = 0;
public function render()
{
$customers = Customer::query()
->with('tenant')
->filterable([ // Same elegant API!
Filter::ilike('name'),
Filter::ilike('legal_name'),
Filter::ilike('email'),
Filter::ilike('tax_id'),
Filter::exact('tax_id_type'),
Filter::exact('tenant_id'),
])
->orderBy('name')
->customPaginate();
return view('livewire.customer-list', compact('customers'));
}
}
<div>
{{-- Filters automatically bind to public properties --}}
<input wire:model.live="name" type="text" placeholder="Nome">
<input wire:model.live="legal_name" type="text" placeholder="Razão Social">
<input wire:model.live="email" type="text" placeholder="Email">
<input wire:model.live="tax_id" type="text" placeholder="CPF/CNPJ">
<select wire:model.live="tax_id_type">
<option value="">Tipo</option>
<option value="cpf">CPF</option>
<option value="cnpj">CNPJ</option>
</select>
{{-- Results --}}
<div class="grid gap-4">
@foreach($customers as $customer)
<div class="border p-4 rounded">
<h3>{{ $customer->name }}</h3>
<p>{{ $customer->email }}</p>
</div>
@endforeach
</div>
{{ $customers->links() }}
</div>
That's it! 🎉 No when() clauses, filters are applied automatically!
O pacote suporta 3 tipos de paginação:
// 1. Padrão (com total count)
->customPaginate('paginate', 15)
// 2. Simples (sem total - mais rápido)
->customPaginate('simple', 20)
// 3. Cursor (mais performático - ideal para datasets grandes)
->customPaginate('cursor', 25)
Controle dinâmico via Livewire:
class CustomerList extends Component
{
use LivewireFilterable;
public string $name = '';
public int $perPage = 15; // ✅ Dinâmico!
public function render()
{
$customers = Customer::query()
->filterable([Filter::ilike('name')])
->customPaginate('cursor', $this->perPage); // ✅ Usa a propriedade
}
}
{{-- Usuário pode escolher --}}
<select wire:model.live="perPage">
<option value="10">10 por página</option>
<option value="25">25 por página</option>
<option value="50">50 por página</option>
<option value="100">100 por página</option>
</select>
Filter::exact('status') // WHERE status = ?
Filter::notEquals('status') // WHERE status != ?
Filter::gt('amount') // WHERE amount > ?
Filter::gte('amount') // WHERE amount >= ?
Filter::lt('amount') // WHERE amount < ?
Filter::lte('amount') // WHERE amount <= ?
Filter::between('created_at') // WHERE created_at BETWEEN ? AND ?
Filter::like('name') // WHERE name LIKE %?%
Filter::ilike('email') // Case-insensitive (database-specific)
Filter::notLike('description') // WHERE description NOT LIKE %?%
Filter::startsWith('sku') // WHERE sku LIKE ?%
Filter::endsWith('domain') // WHERE domain LIKE %?
// PostgreSQL native with GIN index support
Filter::fullText(['title', 'content'], 'search')
->setFullTextLanguage('portuguese')
->setFullTextPrefixMatch(true)
Filter::in('category_id') // WHERE category_id IN (?)
Filter::notIn('status') // WHERE status NOT IN (?)
Filter::isNull('deleted_at') // WHERE deleted_at IS NULL
Filter::isNotNull('verified_at') // WHERE verified_at IS NOT NULL
// Simple relationship
Filter::relationship('category', 'slug', '=', 'category')
->with()
// OR logic (whereAny)
Filter::relationship('tags', 'name')
->whereAny([
['name', '=', 'featured'],
['name', '=', 'sale'],
])
->with()
// AND logic (whereAll)
Filter::relationship('permissions', 'name')
->whereAll([
['name', '=', 'edit-posts'],
['is_active', '=', true],
])
->with()
// NOT logic (whereNone)
Filter::relationship('tags', 'is_banned')
->whereNone([
['is_banned', '=', true],
])
->with()
Filter::json('attributes', 'color', '=', 'color')
->setDatabaseDriver('pgsql')
Filter::json('metadata', 'specs.weight', '>', 'min_weight')
->setDatabaseDriver('mysql')
If your Livewire property name differs from the database column:
public string $searchName = '';
public string $customerEmail = '';
// Second parameter = Livewire property name
Filter::ilike('name', 'searchName')
Filter::ilike('email', 'customerEmail')
public string $created_date = '';
Filter::exact('created_at', 'created_date')
->castDate()
->endOfDay() // Sets time to 23:59:59
Use Livewire's #[Url] attribute to persist filters in the URL:
use Livewire\Attributes\Url;
class CustomerList extends Component
{
use LivewireFilterable;
#[Url]
public string $name = '';
#[Url]
public string $email = '';
#[Url(as: 'type')]
public string $tax_id_type = '';
}
Now filters appear in URL: ?name=john&email=test@&type=cpf
Add debouncing to specific inputs in Blade:
<input wire:model.live.debounce.500ms="name" type="text">
Or programmatically:
Filter::like('name')->debounce(500) // 500ms
public function clearFilters(): void
{
$this->reset(['name', 'email', 'tax_id']);
}
In Blade:
<button wire:click="clearFilters">Limpar Filtros</button>
class ProductList extends Component
{
use LivewireFilterable;
#[Url] public string $search = '';
#[Url] public array $price_range = [];
#[Url] public ?string $category = null;
#[Url] public array $tags = [];
#[Url] public string $created_date = '';
public function render()
{
$products = Product::query()
->filterable([
// Full-text search
Filter::fullText(['name', 'description'], 'search')
->setFullTextLanguage('portuguese'),
// Price range
Filter::between('price', 'price_range'),
// Category relationship
Filter::relationship('category', 'slug', '=', 'category')
->with(),
// Tags with OR logic
Filter::relationship('tags', 'name', 'IN', 'tags')
->with(),
// Date filter
Filter::exact('created_at', 'created_date')
->castDate()
->endOfDay(),
])
->orderBy('created_at', 'desc')
->customPaginate('paginate', 20);
return view('livewire.product-list', compact('products'));
}
public function clearFilters(): void
{
$this->reset(['search', 'price_range', 'category', 'tags', 'created_date']);
}
}
// Migration
Schema::table('products', function (Blueprint $table) {
$table->tsvector('search_vector')->nullable();
});
DB::statement('CREATE INDEX products_search_idx ON products USING GIN(search_vector)');
// Update trigger
DB::statement("
CREATE TRIGGER products_search_update
BEFORE INSERT OR UPDATE ON products
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(search_vector, 'pg_catalog.portuguese', name, description);
");
// Filter
Filter::fullText('search_vector', 'q')
->useTsVector()
->setDatabaseDriver('pgsql')
Performance: 5ms vs 500ms on 1M rows (100x faster!)
Automatically adapts to your database:
ILIKELOWER() functionLIKE (case-insensitive by default)Filter::ilike('email') // Works on all databases!
composer test
Run specific test suites:
composer test:unit
composer test:types
composer test:lint
The MIT License (MIT). Please see License File for more information.
Please see CONTRIBUTING.md for details.
Livewire Filterable - Elegant filtering for Laravel Livewire 🚀