In former posts, we often mentioned how powerful can be a feature, that filters your database results based on the current HTTP query string. Now, let’s take a look at a concrete implementation that’s easy to adopt in any project.
First of all, I need to clear that, the basic idea comes from Laracasts. At this episode, you can take a look at how Jeffrey builds the filters. However, while using it in our own projects we added some other functionality to the filters, so we try to share them with you.
The Basic Concept
The basic concept is very simple. We can control our eloquent query builder by defining different parameters in the current URL. Let’s take an example:
example.app/products?sort[by]=price&sort[order]=asc&status=available&category=tech&keywords=printer
As it’s visible, we want to set the sorting parameters, status, and the category, also we search for the “printer” expression. The question is, how can we do this automatically, instead of explicitly use the $request parameter in a controller. Here is where we can write the dedicated class that creates an interface that we can apply to any model’s eloquent builder.
So, as an end result, we want to see the following code in our controller:
class ProductsController extends Controller { ... public function index(ProductFilter $filter) { $procuts = Product::filter($filter)->paginate(25); ... } }
This means we try to keep the controllers as much it’s possible and move all the filtering and query building code to the dedicated filter class. Let’s see how can we manage that!
The Filter Base Class
Since every eloquent model is different, we will apply different filters for every model. The filters will extend the base filter class, where we handle the request parameters. Let’s create a Filter.php in the app/Http/Filters directory.
<?php namespace App\Http\Filters; use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; abstract class Filter { /** * The request instance. * * @var \Illuminate\Http\Request */ protected $request; /** * The builder instance. * * @var \Illuminate\Database\Eloquent\Builder */ protected $builder; /** * Initialize a new filter instance. * * @param \Illuminate\Http\Request $request * @return void */ public function __construct(Request $request) { $this->request = $request; } /** * Apply the filters on the builder. * * @param \Illuminate\Database\Eloquent\Builder $builder * @return \Illuminate\Database\Eloquent\Builder */ public function apply(Builder $builder): Builder { $this->builder = $builder; foreach ($this->request->all() as $name => $value) { if (method_exists($this, $name)) { call_user_func_array([$this, $name], array_filter([$value])); } } return $this->builder; } }
So what’s happening here? We pick up the request and loop through all of its values. If the current filter has a method named the request parameter’s name, we call the method and pass its value to it.
From here, we have nothing else to do, but to write the filter methods and put the query builder logic we need to implement. Let’s take a look at an example filter.
An Example Filter Class
Let’s use the example URL from the basic concept section and write an example filter for that. We create a product filter that contains the methods for the example URL’s parameters. We should put the filter next to the base class, so we create a new file named ProductFilter.php in the app/Http/Filters directory.
<?php namespace App\Http\Filters; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Schema; class ProductFilter extends Filter { /** * Filter the products by the given string. * * @param string|null $value * @return \Illuminate\Database\Eloquent\Builder */ public function keywords(string $value = null): Builder { return $this->builder->where('name', 'like', "{$value}%"); } /** * Filter the products by the given status. * * @param string|null $value * @return \Illuminate\Database\Eloquent\Builder */ public function status(string $value = null): Builder { return $this->builder->where('status', $value); } /** * Filter the products by the given category. * * @param string|null $value * @return \Illuminate\Database\Eloquent\Builder */ public function category(string $value = null): Builder { return $this->builder->where('category', $value); } /** * Sort the products by the given order and field. * * @param array $value * @return \Illuminate\Database\Eloquent\Builder */ public function sort(array $value = []): Builder { if (isset($value['by']) && ! Schema::hasColumn('products', $value['by'])) { return $this->builder; } return $this->builder->orderBy( $value['by'] ?? 'created_at', $value['order'] ?? 'desc' ); } }
The magic happens here, in these methods. When the method is called with its parameters, we implement the logic that the query builder needs to apply.
If we want to add a new filtering logic, all we need to do is creating a new method and implement the query builder’s logic inside of it. From that point, our filter class is ready to accept the new parameter and its value.
Making Models Filterable
The best way to add this functionality to a model is by using a query scope. With the help of that, we can easily get the query builder object and pass it to the filter class.
Also, filters should be used by any model, so it would be a perfect place to create a trait that we can use in our models. From that point, we can use the filter scope on our model, that will apply all the relevant filters based on the current HTTP query.
Let’s create a Filterable.php file in the app/Concerns directory:
<?php namespace App\Concerns; use App\Http\Filters\Filter; use Illuminate\Database\Eloquent\Builder; trait Filterable { /** * Apply all relevant filters. * * @param \Illuminate\Database\Eloquent\Builder $query * @param \App\Http\Filters\Filter $filter * @return \Illuminate\Database\Eloquent\Builder */ public function scopeFilter(Builder $query, Filter $filter): Builder { return $filter->apply($query); } }
So in our model, we can import the trait and use it:
use App\Concerns\Filterable; class Product extends Model { use Filterable; ... }
And that’s it. From this point, we can use the code we wanted in our controller. All we need to do is calling the filter method and pass the relevant filter instance to it.
Summary
In my opinion, this is a very powerful and flexible approach. You can use it with AJAX requests and normal web requests as well.
So briefly: this approach lets you filtering your database records based on your HTTP request. It’s automated, which means no need for any extra code in your controller if you want to modify your database query. This is available for every model, all you need to do is using the Filterable trait and create the model’s filter that you will use when applying the filters – as we did in the example index() method.