Laravel Distillery provides an elegant way for filtering and paginating Eloquent Models. Distillery taps into native Laravel API Resource Collections and builds a paginated filtered result of models/resources while making it possible to use the Laravel's pagination templates.
You may use Composer to install Distillery into your Laravel project:
composer require matejsvajger/laravel-distillery
After installing Distillery, publish its config using the vendor:publish
Artisan command.
php artisan vendor:publish --tag=distillery-config
After publishing Distillery's config, its configuration file will be located at config/distillery.php
. This configuration file allows you to configure your application setup options and each configuration option includes a description of its purpose, so be sure to thoroughly explore this file.
Let's say you have a long list of products on route: /product-list
all you need to do is attach Distillable
trait to your Product model:
namespace App\Models
use Illuminate\Database\Eloquent\Model;
use matejsvajger\Distillery\Traits\Distillable;
class Product extends Model {
use Distillable;
protected $distillery = [
'hidden' => [
//
],
'default' => [
//
]
];
...
}
Distillable trait adds a static function distill($filters = null)
. In your controller that handles the /product-list
route just replace your Product model fetch call (ie:Product:all()
) with ::distill()
:
class ProductListController extends Controller
{
public function index()
{
return view('product.list',[
'products' => Product::distill()
]);
}
}
distill()
will return a paginated response of 15 items. This is the default Eloquent model value on $perPage
property. You can adjust it by overwriting the value in your model or set a default value for limit
in distillery model property.
To add pagination links to the view call $products->links();
in your blade template:
...
<!-- Your existing list -->
@foreach($products as $product)
<tr>
<td>{{ $product->id }}</td>
<td>{{ $product->name }}</td>
<td>{{ $product->description }}</td>
<td>{{ $product->price }}</td>
</tr>
@endforeach
...
<div class="text-center">
{{ $products->links() }} <!-- Add this. -->
</div>
...
There you have it, a paginated list of Product models.
What!? This is just like Laravel's Paginator! Right, we'll want to filter it too, eh? Ok, carry on.
If we want to filter the above product list with a search query on name
and description
we'll need a search filter for product model. Let's create it:
php artisan distillery:filter Search Product
This will scaffold a Search filter in app/Filters/Product/Search.php
Generated class implements apply(Builder $builder, $value)
method, that receives Eloquent builder and the filter value.
For the above Search example we would do something like this:
namespace App\Filters\Product;
use Illuminate\Database\Eloquent\Builder;
use matejsvajger\Distillery\Contracts\Filter;
class Search implements Filter
{
public static function apply(Builder $builder, $value)
{
return $builder->where(function ($query) use ($value) {
$query
->where('name', 'like', "{$value}%")
->orWhere('description', 'like', "%{$value}%");
});
}
}
To apply the filter to the previous product list you can just add a search query string parameter to the url:
/product-list?search=socks
and the collection will be automatically filtered and pagination links will reflect the set filters.
For more examples on filters check the Examples section.
The idea behind Distillery is that every request parameter is a filter name/value pair. Distillery loops through all request parameters, checks if the filter for selected model exists and builds a filtered resource collection based on their values.
By default Distillery predicts that you have:
- models stored in
app/Models
, - resources stored in
app/Http/Resources
, - and that filters will be in
app/Filters
.
All values are configurable through the config file.
Filter names
- page and
- limit
are reserved for laravel paginator.
Distillery comes with an Artisan generator command that scaffolds your filter classes for existing models. Signature has two parameters:
'distillery:filter {filter} {model?}'
{filter}
Filter name (required){model?}
Model class name (optional)
If you pass in the model name the filter will be generated in the sub-namespace: App\Filters\{Model}
. Without optional model paramater the filteres are generated in App\Filters
for general usage on multiple models.
To enable fallback to general filters you need to set 'fallback' => true
on the distillery model property.
During generation you'll be offered to choose from some standard filter templates:
The generated class returns
$builder
without modifications. You'll need write the logic yourself.
You define a list of model fields you wish to sort on and select a default sorting field and direction.
Define the model field to search on. A filter with
"like {$value}%"
will be generated.
Sometimes you'll want to attach additional filters on server-side. By default you don't need to pass any filters in. Distilliery will pull them out of request. Usually you'll have a seo route, that already should return a fraction of models instead of all; like a category route for products: /category/summer-clothing?search=bikini
.
Normally you wouldn't want to pass the category id in paramaters since it's already defined with a seo slug.
You can add aditional filters not defined in URI or overwrite those by passing an array into distill function. i.e.: If you have a Category filter that accepts an id, attach it in your controller:
public function list(Request $request, Category $category)
{
return view('product.list',[
'products' => Product::distill([
'category' => $category->id,
]);
]);
}
Distillery comes with a facade that gives you an option to distill any model without the Distillable trait. It takes two parameters, Model FQN and filter array.
Distillery::distill(Product::class, $filters);
If you're using Distillery as API endpoints you probabbly don't want to expose your whole model to the world or maybe you want to attach some additional data. Distillery checks if Eloquent resources exist and maps the filtered collection to them, otherwise it returns normal models.
If you don't have them just create them with Artisan
php artisan make:resource Product
and check out the docs on how to use them.
It is possible to define default filter values per model. For example if you want a default filter value for some model you can do it with a 'default' key in a protected $distillery
array on the model itself:
class User extends Model {
protected $distillery = [
'default' => [
'sort' => 'updated_at-desc'
]
];
}
There is a 'hidden'
config array available on model to hide filters from URI when those are applied serverside:
class User extends Model {
protected $distillery = [
'hidden' => [
'category' // - applied in controller; set from seo url
]
];
}
A model can use a general filter if one in it's namespace isn't defined:
class User extends Model {
protected $distillery = [
'fallback' => true
];
}
Distillery comes with a standard filtering route, where you can filter/paginate any model automatically without attaching traits to models.
This functionality is disabled by default. You need to enable it in the config.
Default route for filtering models is /distill
use it in combination with model name and query string:
/distill/{model}?page=1&limit=10&search=socks
Models filterable by this route need to be added to the distillery.routing.models
config array:
'routing' => [
'enabled' => true,
'path' => 'distill',
'middleware' => [
'web',
],
'models' => [
'product' => App\Models\Product::class,
]
],
It's possible to change the route path in the config. If you want to protect it with Auth for example, you may also attach custom middleware to the route in config.
Pagination links are part of the Laravel pagination. Check out the Laravel docs on how to customize them.
For sorting a model on multiple fields you would have a sort filter with values something like this: sort=field-asc
and sort=field-desc
:
php artisan distillery:filter Sort Product
class Sort implements Filter
{
protected static $allowed = ['price', 'name', 'updated_at'];
public static function apply(Builder $builder, $value)
{
if (Str::endsWith($value, ['-asc', '-desc'])) {
[$field, $dir] = explode('-', $value);
if (in_array($field, static::$allowed)) {
return $builder->orderBy($field, $dir);
}
}
return $builder->orderBy('updated_at', 'desc');
}
}
And apply to apply the filter just add it to the qs: /product-list?search=socks&sort=price-desc
.
Sometimes you'll want to filter on relations of the model.
Suppose you have a Product model with multiple colors attached:
class Color implements Filter
{
public static function apply(Builder $builder, $value)
{
$value = is_array($value) ? $value : [$value];
$query = $builder->with('colors');
foreach ($value as $colorId) {
$query->whereHas('colors', function ($q) use ($colorId) {
$q->where('id', $colorId);
});
}
return $query;
}
}
And to apply it: /product-list?search=socks&sort=price-desc&color[]=2&color[]=5
.
- Add possibility to generate standard predefined filters (sort, search, ...).
- Make possible to define which paramateres to hide from url query strings.
- Add fallback to general filters that can be re-used across different models.
- Write tests.
Laravel Distillery is open-sourced software licensed under the MIT license.