Managing Filters in Laravel
Oliver Sarfas • January 6, 2019
programming laravelFiltering your Models in Laravel is a common question, and often comes with a lot of "enthusiasm" from each side.
Do you filter in your;
- Controller?
- Model?
- A Trait?
- Some service?
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)