Simple facet filtering in Laravel projects, hassle free.
mgussekloo/laravel-facet-filter is a Laravel package for simple facet filtering in laravel projects, hassle free..
It currently has 60 GitHub stars and 805 downloads on Packagist.
Install it with composer require mgussekloo/laravel-facet-filter.
Discover more Laravel packages by mgussekloo
or browse all Laravel packages to compare alternatives.
Last updated
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.

Please contribute to this package either by creating a pull request or reporting an issue.
This package can be installed through Composer.
composer require mgussekloo/laravel-facet-filter
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.
]
];
}
}
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
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();
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.
$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'
]
*/
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
$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
}
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
]
];
}
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);
Example:
$products = Product::facetFilter($filter)->paginate(10);
$pagination = $products->appends(request()->input())->links();
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
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',
],
The MIT License (MIT). Please see License File for more information.