matejsvajger/laravel-distillery

An elegan way of filtering Eloquent models.

v0.4.0 2018-11-14 23:02 UTC

This package is auto-updated.

Last update: 2024-09-17 11:19:34 UTC


README

Total Downloads License

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.