mgussekloo / laravel-facet-filter
Simple facet filtering in Laravel projects, hassle free.
Requires
- php: ^8.0
- spatie/laravel-package-tools: ^1.9.2
Requires (Dev)
- laravel/pint: ^1.6
- nunomaduro/larastan: ^2.0
- orchestra/testbench: ^8.0
This package is auto-updated.
Last update: 2025-07-15 17:38:38 UTC
README
Laravel Facet Filter
This package provides simple facet filtering (sometimes called Faceted Search or Faceted Navigation) in Laravel projects. It helps narrow down query results based on the attributes of your models.
- Free, no dependencies
- Easy to use in any project
- Easy to customize
- There's a demo project to get you started
- Ongoing support (last update: july 2025)
Contributing
Please contribute to this package either by creating a pull request or reporting an issue.
Installation
This package can be installed through Composer.
composer require mgussekloo/laravel-facet-filter
Get started
Update your models
Add a Facettable trait and a facetDefinitions() method to models you'd like to filter:
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Mgussekloo\FacetFilter\Traits\Facettable; class Product extends Model { use HasFactory; use Facettable; public static function facetDefinitions() { // Return an array of definitions return [ [ 'title' => 'Main color', // The title will be used for the parameter. 'fieldname' => 'color' // Model property from which to get the values. ], [ 'title' => 'Sizes', 'fieldname' => 'sizes.name' // Use dot notation to get the value from related models. ] ]; } }
Build an index
In most cases you'll want to create an index to filter large datasets efficiently. (Don't want to build an index? Skip ahead to filtering collections.)
First, run the migrations:
php artisan vendor:publish --tag="facet-filter-migrations"
php artisan migrate
To build the index, create an Artisan command that queries your Facettable models. Run it periodically or whenever your data changes.
$products = Product::with(['sizes'])->get(); // get some products $products->buildIndex(); // build the index
Get results
Within a controller, apply the facet filter to a query
$filter = request()->all(); // the filter looks like ['main-color' => ['green']] $products = Product::facetFilter($filter)->get();
Basic frontend example
Here's a simple demo project that demonstrates a basic frontend.
<div class="flex"> <div class="w-1/4 flex-0"> @foreach ($products->getFacets() as $facet) <p> <h3>{{ $facet->title }}</h3> @foreach ($facet->getOptions() as $option) <a href="?{{ $option->http_query }}" class="{{ $option->selected ? 'underline' : '' }}">{{ $option->value }} ({{ $option->total }}) </a><br /> @endforeach </p><br /> @endforeach </div> <div class="w-3/4"> @foreach ($products as $product) <p> <h1>{{ $product->name }} ({{ $product->sizes->pluck('name')->join(', ') }})</h1> {{ $product->color }}<br /><br /> </p> @endforeach </div> </div>
To see an example of a Livewire implementation, see this gist.
Facet details
$facets = $products->getFacets(); /* You can filter and sort like any regular Laravel collection. */ $singleFacet = $facets->firstWhere('fieldname', 'color'); /* Find out stuff about the facet. */ $paramName = $singleFacet->getParamName(); // "main-color" $options = $singleFacet->getOptions(); /* Options look like this: (object)[ 'value' => 'Red', 'selected' => false, 'total' => 3, 'slug' => 'color_red', 'http_query' => 'main-color%5B1%5D=red&sizes%5B0%5D=small' ] */
Customization
Advanced indexing
Extend the Indexer to customize behavior, e.g. to save a "range bracket" value instead of a "individual price" value to the index.
class MyCustomIndexer extends \Mgussekloo\FacetFilter\Indexer { public function buildValues($facet, $model) { $values = parent::buildValues($facet, $model); if ($facet->fieldname == 'price') { if ($model->price > 1000) { return 'Expensive'; } if ($model->price > 500) { return '500 - 1000'; } if ($model->price > 250) { return '250 - 500'; } return '0 - 250'; } return $values; } }
You may overwrite the indexer() method on your Facettable model to return an instance of your custom indexer:
use App\MyCustomIndexer; class Product extends Model { use HasFactory; use Facettable; public static function indexer() { return new MyCustomIndexer(); } }
$products = Products::get(); $products->buildIndex(); // uses MyCustomIndexer
Updating a larger index over time
$indexer = new Indexer(); $perPage = 1000; $currentPage = Cache::get('facetIndexingPage', 1); $products = Product::with(['sizes'])->paginate($perPage, ['*'], 'page', $currentPage); if ($currentPage == 1) { $indexer->resetIndex(); // clear entire index } $indexer->buildIndex($products); if ($products->hasMorePages()) {} // next iteration, increase currentPage with one }
Custom facets
Provide custom attributes and an optional custom Facet class in the facet definitions.
public static function facetDefinitions() { return [ [ 'title' => 'Main color', 'description' => 'The main color.', // optional custom attribute, you could use $facet->description when creating the frontend... 'related_id' => 23, // ... or use $facet->related_id with your custom indexer 'fieldname' => 'color', 'facet_class' => CustomFacet::class // optional Facet class with custom logic ] ]; }
Filtering collections
It's possible to apply facet filtering to a collection without building an index. Facettable models return a FacettableCollection, which provides an indexlessFacetFilter() method.
$products = Product::all(); // returns a "FacettableCollection" $products = $products->indexlessFacetFilter($filter);
Pagination
Example:
$products = Product::facetFilter($filter)->paginate(10); $pagination = $products->appends(request()->input())->links();
Notes on caching
By default Facet Filter caches some heavy operations through the non-persistent 'array' cache driver. Caches are based on the model and filter, not the query bindings or specifics. Use a cacheTag to distinguish between queries.
// if you have two facet filter queries on the same model... Product::where('published', false)->cacheTag('unpublished')->facetFilter($filter)->get(); // ...make sure to distinguish between queries Product::where('published', true)->cacheTag('published')->facetFilter($filter)->get();
The default Indexer clears the cache for all models when rebuilding the index. You can do it manually:
Product::forgetCache(); // clears cache for all cache tags Product::forgetCache('unpublished'); // clears cache for only this cachetag use Mgussekloo\FacetFilter\Facades\FacetCache; FacetCache::forgetCache(); // clears cache for all models and cache tags
Config
You can configure a peristent cache driver through config/facet-filter.php
. Be aware of any caching related issues, e.g. if you have any user specific results this may be problematic.
php artisan vendor:publish --tag=facet-filter-config
'classes' => [ 'facet' => Mgussekloo\FacetFilter\Models\Facet::class, 'facetrow' => Mgussekloo\FacetFilter\Models\FacetRow::class, ], 'table_names' => [ 'facetrows' => 'facetrows', ], 'cache' => [ 'expiration_time' => \DateInterval::createFromDateString('24 hours'), 'key' => 'mgussekloo.facetfilter.cache', 'store' => 'array', ],
License
The MIT License (MIT). Please see License File for more information.