matejsvajger / laravel-distillery
An elegan way of filtering Eloquent models.
Requires
- php: ^7.1.3
Requires (Dev)
- orchestra/testbench: ^3.7
- phpunit/phpunit: ^7.4
This package is auto-updated.
Last update: 2024-11-17 11:48:28 UTC
README
Introduction
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.
Installation & Configuration
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.
Quickstart
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() ]); } }
Pagination
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.
Filtering
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.
How it works
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.
Digging deeper
Artisan Command distillery:filter
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:
Blank template
The generated class returns
$builder
without modifications. You'll need write the logic yourself.
Sorting template
You define a list of model fields you wish to sort on and select a default sorting field and direction.
Search template
Define the model field to search on. A filter with
"like {$value}%"
will be generated.
Serverside filter values
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 Facade
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);
Model Resources
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.
Default filter values per model
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' ] ]; }
Hide filters from URI QueryString
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 ] ]; }
Enable fallback to general filters
A model can use a general filter if one in it's namespace isn't defined:
class User extends Model { protected $distillery = [ 'fallback' => true ]; }
API Filter-Pagination Route
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.
Customizing pagination links
Pagination links are part of the Laravel pagination. Check out the Laravel docs on how to customize them.
Examples
Sorting
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
.
Filtering relations
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
.
Roadmap to 1.0.0
- 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.
License
Laravel Distillery is open-sourced software licensed under the MIT license.