devaction-labs / livewire-filterable
A Laravel Livewire package for elegant, reactive model filtering with zero boilerplate
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/devaction-labs/livewire-filterable
Requires
- php: ^8.5.0
- illuminate/database: ^12
- illuminate/support: ^12
- livewire/livewire: ^4
Requires (Dev)
- laravel/framework: ^12.40
- laravel/pint: ^1.24.0
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.8
- pestphp/pest: ^4.1.0
- pestphp/pest-plugin-laravel: ^4.0
- pestphp/pest-plugin-type-coverage: ^4.0.2
- phpstan/phpstan: ^2.1.26
- rector/rector: ^2.1.7
- symfony/var-dumper: ^7.3.3
This package is auto-updated.
Last update: 2026-01-27 21:22:02 UTC
README
A Laravel Livewire package for elegant, reactive model filtering with zero boilerplate. No more manual when() clauses!
✨ Features
- 🎯 Zero Boilerplate - No manual
when()clauses needed - ⚡ Reactive - Filters update automatically with Livewire
- 🔥 Elegant API - Same simple syntax:
Filter::like('name') - 🚀 PHP 8.5+ - Uses cutting-edge PHP features
#[\NoDiscard]attribute on all fluent methods (30+ methods)- Pipe operator
|>for data transformation pipelines - Laravel 12's
#[Scope]attribute for cleaner scopes
- 📦 Laravel 11 & 12 - Full support
- 🎨 Livewire 4 - Built for the latest version
- 🔍 Full-Text Search - PostgreSQL native support with GIN indexes
- 🔗 Relationships - Filter with
whereAny,whereAll,whereNone - 📅 Smart Dates - Carbon integration with
startOfDay/endOfDay - 🗄️ JSON Fields - Database-agnostic JSON filtering
- 🎭 Type-Safe - Full PHP 8.5 type coverage
📦 Installation
composer require devaction-labs/livewire-filterable
Requirements:
- PHP 8.5+
- Laravel 11 or 12
- Livewire 4
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.
🚀 Quick Start
1. Add traits to your Model
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; }
2. Create a Livewire Component
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')); } }
3. Create the Blade View
<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!
📚 Documentation
- 📘 How It Works - Understand how automatic property binding works
- 🚀 Laravel 12 & PHP 8.5 Features - Modern features used in the package
- 🔮 PHP 8.5 Full Features Example - Future implementation with all PHP 8.5 features
- 💡 Practical Examples - Real-world usage examples
🔄 Paginação
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>
📖 Available Filters
Basic Comparison Filters
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 ?
Text Search Filters
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 %?
Full-Text Search
// PostgreSQL native with GIN index support Filter::fullText(['title', 'content'], 'search') ->setFullTextLanguage('portuguese') ->setFullTextPrefixMatch(true)
Array Filters
Filter::in('category_id') // WHERE category_id IN (?) Filter::notIn('status') // WHERE status NOT IN (?)
Null Filters
Filter::isNull('deleted_at') // WHERE deleted_at IS NULL Filter::isNotNull('verified_at') // WHERE verified_at IS NOT NULL
Relationship Filters
// 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()
JSON Field Filters
Filter::json('attributes', 'color', '=', 'color') ->setDatabaseDriver('pgsql') Filter::json('metadata', 'specs.weight', '>', 'min_weight') ->setDatabaseDriver('mysql')
🎯 Custom Property Names
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')
📅 Date Handling
public string $created_date = ''; Filter::exact('created_at', 'created_date') ->castDate() ->endOfDay() // Sets time to 23:59:59
🔗 URL Persistence
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
⏱️ Debouncing
Add debouncing to specific inputs in Blade:
<input wire:model.live.debounce.500ms="name" type="text">
Or programmatically:
Filter::like('name')->debounce(500) // 500ms
🧹 Clear Filters
public function clearFilters(): void { $this->reset(['name', 'email', 'tax_id']); }
In Blade:
<button wire:click="clearFilters">Limpar Filtros</button>
🎨 Complete Example
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']); } }
🗄️ Database-Specific Features
PostgreSQL Full-Text Search with GIN Index
// 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!)
Case-Insensitive Search (ILIKE)
Automatically adapts to your database:
- PostgreSQL: Native
ILIKE - MySQL:
LOWER()function - SQLite: Standard
LIKE(case-insensitive by default)
Filter::ilike('email') // Works on all databases!
🧪 Testing
composer test
Run specific test suites:
composer test:unit composer test:types composer test:lint
📝 License
The MIT License (MIT). Please see License File for more information.
🤝 Contributing
Please see CONTRIBUTING.md for details.
🙏 Credits
Livewire Filterable - Elegant filtering for Laravel Livewire 🚀
