Managing Filters in Laravel cover image

Managing Filters in Laravel

Oliver Sarfas • January 6, 2019

programming laravel

Filtering your Models in Laravel is a common question, and often comes with a lot of "enthusiasm" from each side.

Do you filter in your;

All have their valid reasons to be used. However my method is a little "different" let's say. It's a a little like using a Service, but also not (as it's not explicitly loaded using a ServiceProvider

It's easier to show, instead of trying to explain.

Setup

So, we have an Application, let's say a Forum. It has Users and Content. Pretty simple.

We have a ContentController that handles requests to the /content endpoint.

For this example, we'll use GET /content which will be bound to the ContentController@index method.

Implementation

Firstly, we need to add a Trait to our Model, in this case, it'll be the Content model. The trait is;

<?php

namespace App\Traits;

use App\Filters\Filters;

trait HasFilters
{
    public function scopeFilter( $query, Filters $filters )
    {
        return $filters->apply( $query );
    }
}

This enables us to use the Model::filter(); method. So we add this to the Content model, like so;

[...]

class Thread extends Model
{
    use HasFilters;

    [...]
}

We create an Abstract Class called Filters, and place it in the App\Filters namespace. It will look like this;

<?php

namespace App\Filters;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;

abstract class Filters
{
    protected $request, $builder;
    protected $filters = [];

    /**
    * Filters constructor.
    * @param Request $request
    */
    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    /**
    * @param Builder $builder
    *
    * @return Builder
    */
    public function apply(Builder $builder)
    {
        $this->builder = $builder;

        foreach ($this->getFilters() as $filter=>$value) {
            if (method_exists($this, $filter) && notNullValue($value)) {
                $this->$filter($value);
            }
        }

        return $this->builder;
    }

    /**
    * @return array
    */
    public function getFilters()
    {
        return $this->request->only($this->filters);
    }
}

In a nutshell, this class will take a Request object, and check for any given filters, that are defined in the $filters array.

Any that are defined, are checked against the methods, and if the method exists, the filter is applied.

Working Example - Popularity

Back to our Controller, and we want to add a GET parameter, to order our content by popularity, ?popular=true, or some equivalent

To do this, we will create a class called ContentFilters. We use this, so that we can easily establish what filters are available, and is separates the concerns away from the Model, and the Controller.

Our class, will look like this;

<?php

namespace App\Filters;

use App\User;

class ContentFilters extends Filters
{
    protected $filters = [
        'popular',
    ];

    protected function popular()
    {
        return $this->builder->orderBy('replies_count', 'desc');
    }
}

From here, we just adjust the ContentController@index method.

/**
* @param ContentFilters $filters
*
* @return \Illuminate\Http\Response
*/
public function index(ContentFilters $filters)
{
    $content= $this->getContent($filters);

    return view('content.index', compact('content'));
}

protected function getContent(Filters $filters)
{
    return Content::latest()->filter($filters);
}

And that's it. We now have fully configurable, and maintainable filters on any model.

I will be preparing this as a composer package, and hosting it on GitHub shortly, however I know that there are a few "holes" in this current implementation (validation and data cleansing being one)

Questions? Want to talk? Here are all my social channels