How to implement filters for an eCommerce website products page using and obeying the SOLID design principles.

In web applications or websites that displays lists, there is a need to provide filters for easy search and quick finding. Example, filter by name, size, color, name, category, price etc.

Usually you would send this filters as a query parameter and then in your controller include in a where clause the respective requested filters. Example:

Basically for each filter you have to include the if statements and add the query conditions to get the products. This is very hard to refactor since there is a lot of repetitive code. If you have other pages that require filters you have to copy and paste the same code or rewrite the same logic all over again, which will bring a nightmare when debugging.

The better way

1. Create base filter class

Create a base filter class that will have the general logic required iterate through the array of filters and apply each filter logic to the query builder instance of the models you are trying to fetch. This class will do mainly three things.

  1. Get values passed in the filter. Example if maximum_price=2000, the class will have to acquire the filter keys and value pairs.
  2. Resolve the filter class from the filter name. All filters keys are in snake case(maximum_price), filters classes are in studly case ending with ‘filter’(MaximumPriceFilter). This class will be resolved and its instance will be given.
  3. Pass the filter value to the filter method of the filter class and get the new query builder class returned to the model, this is important because we are going to employ Laravel scopes for this.
class BaseFilter{

  protected $declared_filters;

  public function __construct($filters){
    $this->request = request();
    $this->declared_filters = $filters;
  }
  
  public function filter(Builder $builder){
    foreach ($this->getRequestedFilters() as $filter => $value){
      $this->resolveFilterClass($filter)->filter( $builder, $value);
    }
    return $builder;
  }

  public function getRequestedFilters(){
    return array_filter($this->request->only($this- >declared_filters));
  }
  
  public function resolveFilterClass($declared_filter_key){ 
   $filter_name = studly_case($declared_filter_key);
   $class_name = "\\App\Filters\\".$filter_name."Filter";
   $class_instance = App::make($class_name);
   return $class_instance;
  }
}

2. Add the canFilter scope to your model

The can filter scope is used to add the filtration functionality to your models, this scope instead of using the normal $builder instance to add the queries, it will use filter class instance that will return the query builder instance that has the filters applied. Add this scope to the model that you want to be filtered.

public function scopeCanFilter(Builder $builder, $filters){
  return (new BaseFilter($filters))->filter($builder);
}

3. Create classes for your filters

We will create two classes for the previous example, for sort_by_name and sort_by_price. In your front end you will submit the form containing filters and as usual in your server you’ll have the key value pairs. Example

https://website.com?sort_by_name=asc&&sort_by_price=asc

Create the folder Filters/ inside the app/ folder. The filter class name are in studly case with a Filter suffix.

class SortByNameFilter{

  public function filter($builder, $value){ 
    return $builder->sortBy('name', $value);
  }
}

4. Usage inside the controller

Now inside the controller the products can be fetched as simple as:

public function products(){
  $filters = ['sort_by_name','sort_by_price'];

  $posts = Product::canFilter($filters)->get();

  return view('products', compact('posts'));
}

Your controller remains very neat and only deals with handling data sent by the user, querying the models and returning the results to the view. This implementation abides to the following solid principles:

  1. Single responsibility principle: The filter classes have a single responsibility that is to filter the queries, the model stick to database handling and controller knows none of these!
  2. Open-closed principle: The filtration now can be extended to create a new class then the new logic can be applied in the new class, without having to modify the original filter class. This allows to modify the class without having to worry about refactoring the classes dependent on the filter.
  3. Liskov substitution principle: The filters can easily be swapped in the controller or models only by changing the filter names passed from the client side.

Conclusion:

This method has a considerable amount of boiler plate code but, in large projects that have a lot of modifications and refactors, in long term this method is the best to use to implement filters.

https://medium.com/@mwakalingajohn/laravel-7-slack-debugging-rest-api-and-importance-of-logging-using-slack-channel-fc96db23aa4e+

https://medium.com/@mwakalingajohn/laravel-7-datatables-net-super-tables-in-4-steps-c0b544e28bd