Dynamic Filters With Laravel Eloquent
Tags: laravel, eloquent, php
Published: 2021-02-26 13:16:08


# Dynamic Filters With Laravel Eloquent



Did you ever had a Laravel application and needs filters over your API? You might have tried GraphQL, however, you could write a simple one and don’t need any integrations.

In this article I’m going to implement a simple service which could work with Laravel eloquent ORM, it would handle relations and also the basic operators which uses the most.

## The Idea

You are developing a RESTful API and want to let your users filter the indexes with different fields or even on the relations of your model.

For instance, a simple article model which has so many tags and comments , also a user who owns it. Now you have an API which lists all the articles /api/articles . How could you let the user filter over the tags or comments? How could you filter all the articles of a specific user? Moreover, how could you combine all of them together?

Let’s add a filter to our API to find all the articles of user with id = 1.

`/api/articles?filters[]=user.id=1`

Seems cool? Now I want the articles of user 1 which have a specific tag.

`/api/articles?filters[]user.id=1&filters[]=tags.name=ipsa`

In the next step, we are going to setup the basic project and then start adding these dynamic filters to that.

## Setup

I was looking for a real world sample project and found out there is a complete Laravel application which could be used. Take a look at their github repository and walk through the *README *to set it up.

[https://github.com/gothinkster/laravel-realworld-example-app](https://github.com/gothinkster/laravel-realworld-example-app)

## Extend the Idea

We know that there is a query param called filters which contains the relation/field, operator and the value.

Here we are going to implement the basic logical operators which are `=` , `!=` , `>` , `<` , `>=` and `<=` . Also I have added 2 custom operators `~` and `()` .

`~` would work as LIKE in queries with % around it.

e.x. `/api/articles?filters[]=slug~real-world`

SQL: `SELECT * FROM articles where slug like '%real-world%';`

`()` would work as IN in queries.

e.x. `/api/articles?filters[]user.id()1,2`

SQL: `SELECT * FROM articles where user_id IN(1,2);`

## Dive into coding

## The Service

First we need to create a service which could take the request and create the appropriate queries on given model for us.

Create a Services directory under app folder, then create a FilterQueryBuilder.php file inside it.

    # app/Services/FilterQueryBuilder.php

    <?php
    namespace App\Services;

    use Illuminate\Database\Eloquent\Builder;

    class FilterQueryBuilder {
      protected $request;

      public function __construct($request) {
         $this->request = $request;
      }
    }

Now let’s add the code step by step.

We need a public method on this class which would accept an eloquent builder, parse the filters and add the appropriate queries to it. Now let’s add the method below to our service.

    # app/Services/FilterQueryBuilder.php
    ...

    public function buildQuery($query) {
        $filters = $this->request->query('filters');
        $filters = $this->parseFilters($filters);
        foreach($filters as $filter) {
          $query = $this->addFiltersToQuery($query, $filter);
        }

      return $query;
    }
    ...

## Parsing the filters

For each given filter in the query parameter, we need to find the relations/field, operator and the value then we are able to add these to our query.

Let’s add a getOperator method which accept the filter string value and return the operator for us.

    # app/Services/FilterQueryBuilder.php
    ...
    private function getOperator($filter) {
        $operatorsPattern = '/=|!=|\(\)|>=|<=|~|>|</';
        $operator = [];
        preg_match($operatorsPattern, $filter, $operator);
        if(count($operator) == 1) {
          return $operator[0];
        }
    }
    ...

Thanks to regular expressions, it’s super easy to find the operator. If you are not familiar with regex, read more about it [here](https://www.php.net/manual/en/function.preg-match.php). Also you could practice these methods online with [PHP Live Regex](https://www.phpliveregex.com/) website.

Now that we have the operator, we could split the filter string into relations/field and value part. Let’s add the code for it.

    # app/Services/FilterQueryBuilder.php
    ...

    private function getFilterValue($filter, $operator) {
        $result = explode($operator, $filter);
        if(count($result) == 2) {
          return $result[1];
        }
    }

    private function getFilterRelations($filter, $operator) {
        $result = explode($operator, $filter);
        if(count($result) == 2) {
          return $result[0];
        }
    }
    ...

### Relation Tree

So far we have found the relations, operator and value in our filter. How we could add these relation to our query?

For example, for our first API example, we need to create a nested array like this.

    # /api/articles?filters[]user.id()1,2

    Array
    (
        [user] => Array
            (
                [field] => id
                [operator] => ()
                [value] => 1,2
            )

    )

For creating the relation tree, we need the relation keys, operator and value which have founded already.

    # app/Services/FilterQueryBuilder.php
    ...
    private function createRelationTree(string $relationKeys, $operator, $value) {
        $relations = explode('.', $relationKeys);
        $lastKey = array_key_last($relations);
        $field = $relations[$lastKey];
        unset($relations[$lastKey]);
        $result = [];
        if(count($relations) > 0 ) {
          $result[$relations[count($relations)-1]] = [
            'field' => $field,
            'operator' => $operator,
            'value' => $value
          ];
          for($i=count($relations)-2; $i>-1; $i--)
          {
            $result[$relations[$i]] = $result;
            unset($result[$relations[$i+1]]);
          }
        } else {
          $result = [
            'field' => $field,
            'operator' => $operator,
            'value' => $value
          ];
        }

      return $result;
    }
    ...

This code will split all the relation and convert it into a nested array. If we have a filter like `model1.model2.model3.id=100` then the output would be something like this.

    Array
    (
        [model1] => Array
            (
                [model2] => Array
                    (
                        [model3] => Array
                            (
                                [field] => id
                                [operator] => =
                                [value] => 100
                            )

                     )

             )

    )

Now let’s add these methods into one method and aggregate all the filters together.

    # app/Services/FilterQueryBuilder.php
    ...
    private function parseFilters($filters) {
        if(empty($filters)) {
          return [];
        }
        $result = [];
        foreach($filters as $filter) {
          if(empty($filter)) {
            continue;
          }
          $operator = $this->getOperator($filter);
          $value = $this->getFilterValue($filter, $operator);
          $keys = $this->getFilterRelations($filter, $operator);
          $relationTree = $this->createRelationTree($keys, $operator, $value);
          array_push($result, $relationTree);
        }

      return $result;
    }
    ...

## Adding filter to query builder

Now that we have all the filters into an array, we need to add these relations to our query builder.

    # app/Services/FilterQueryBuilder.php
    ...
    private function addFiltersToQuery($query, $filters) {
        if(count($filters) === 3) {
          switch($filters['operator']) {
          case '()':
            return $query->whereIn($filters['field'], explode(',', $filters['value']));
          case '~':
            return $query->where($filters['field'], 'LIKE', '%' . $filters['value'] . '%');
          default:
            return $query->where($filters['field'], $filters['operator'], $filters['value']);
          }
        }
        $relation = array_key_first($filters);
        return $query->whereHas($relation, function(Builder $query) use($relation, $filters) {
          $this->addFiltersToQuery($query, $filters[$relation]);
        });
    }
    ...

Here is a recursive method, which will iterate over the filters array, if it’s a relation then it would use `whereHas` which tells the eloquent builder to join over the tables based on the model definition.

Moreover, when it gets to the end of the array, it will add the field and value with given operator into it. You could see we have handled the custom operators as well.

You could also read more about the [Eloquent](https://laravel.com/docs/8.x/eloquent) and [laravel query builder](https://laravel.com/docs/8.x/queries).

### Service Provider
> Service providers are the central place of all Laravel application bootstrapping. Your own application, as well as all of Laravel’s core services, are bootstrapped via service providers.

You could read more about the service provider idea [here](https://laravel.com/docs/8.x/providers).

The service provider will help you inject your service with laravel service container into your application. It would take care of creating the instance and pass it to your classes and methods.

You need to run the command below to create a service provider called `FilterServiceProvider` .

    $ php artisan make:provider FilterServiceProvider

Then take a look at the file below.

    # app/Providers/FilterServiceProvider.php

    <?php

    namespace App\Providers;

    use Illuminate\Support\ServiceProvider;

    class FilterServiceProvider extends ServiceProvider
    {
        /** 
         * Bootstrap the application services.
         *
         * [@return](http://twitter.com/return) void
         */
        public function boot()
        {   
            //
        }

    /** 
         * Register the application services.
         *
         * [@return](http://twitter.com/return) void
         */
        public function register()
        {   
           //
        }
    }

Now let’s say how we are making an instance from our `FilterQueryBuilder` service inside the register method.

    # app/Providers/FilterServiceProvider.php
    ...
        public function register()
        {
          $this->app->bind(\App\Services\FilterQueryBuilder::class, function () {
            $request = app(\Illuminate\Http\Request::class);

             return new FilterQueryBuilder($request);
          });
    }
    ...

So whenever in the application, you type hinted the `FilterQueryBuilder` class, it will pass the `\Illuminate\Http\Request` object into the constructor and give you an instance.

After you have added your service provider, you need to register it so laravel would understand it. Now open the config/app.php file, and under the providers key, add your provider as well.

    # config/app.php
    ...
    'providers' => [
       ...
           App\Providers\FilterServiceProvider::class,
       ...

    ]

    ...

## How to use it now?

So far, so good. Now you could take a look at the article index API code under `app/Http/Controllers/Api/ArticleController.php`

    # app/Http/Controllers/Api/ArticleController.php
    ...
        public function index(ArticleFilter $filter)
        {
            $articles = new Paginate(Article::loadRelations()->filter($filter));

            return $this->respondWithPagination($articles);
        }

    ...

However they have created an `ArticleFilter` class, but if you take a look at the code, it would just handle some given relation. We are going to replace it with our `FilterQueryBuilder` which would accept any filter on our model based on the model definition. So in future we just need to update our model and the filters would work on them as well.

    # app/Http/Controllers/Api/ArticleController.php

    ...
    public function index(\App\Services\FilterQueryBuilder $filters)
    {   
      $articles = $filters->buildQuery(Article::loadRelations());
      $result = new Paginate($articles);
     
      return $this->respondWithPagination($result);
    }
    ...

## Be Careful !

You should know that this was just a practice and it’s not optimized for large scale applications and also needs to handle so many other things as well. Feel free to communicate and extend it if you like.