Laravel 8: how to manage filters without JS frameworks

Davide Cariola
6 min readDec 29, 2021

Surely one of the most requested features is the setting of dedicated filters, especially when we talk about e-commerce, ad sites and the like. It is also possible, of course, use the same logic for other types of filters (in a blog we can filter topics, or users, for instance).

But how to do it in Laravel, without knowing in depth JavaScript or one of his frameworks?

Let’s find out!

- Class-based components

A good idea to keep everything clean is to work with a component, which we will call filters.

We can actually pass data inside a component through his HTML attributes. However, within these attributes we can store only simple types of data (string, integer, etc).
In this case of study, we have to work with complex types of data (objects, collections, arrays).

Let’s discover an important Laravel feature: Class Based Components.

First of all, let’s create one:

//in bashphp artisan make:component Filter

This command will create a folder in app/View/Components, with a PHP class inside of it.

In this class we will create a public attribute and then we’re going to manage it through the constructor class. Thinking of an Ad Site, I’d like to implement a simple “search by category” filter, where Category is an actual Model (and, therefore, an object).

//in Filter.phpclass Filter extends Component{   public $categories;   
public function __construct(Collection $categories){
$this->categories = $categories;
}
public function render(){
return view('components.filter');
}
}

The render() function, on the other hand, automatically creates a new component named filter inside the resources/views/components folder.

Now we have to actually render it, placing it where we want it:

//in blade file<x-filter :categories="$categories" />

Using this syntax, the : prefix, we inform the component that it will not have to accept simple data within the categories attribute but rather complex data (therefore also PHP expressions or variables).

Once rendered, we can simply work in his blade file as usual.

- Filters

Now we can actually manage our filters.

The best idea, to ensure that their queries can be chained, is to work in a single form, so as to manage everything with a single request.

Actually, we have two possibilities: to allow the user to choose from various filters and then submit the form with a button, or to have the form submitted at every user choice (e.g., Amazon).

We choose this second, more dynamic approach.

Let’s start with the filter related to the category of the ad, as we said above, to which we will then integrate the one relating to the date of insertion.

An excellent idea is certainly to create a separate controller, which we will call FilterController for convenience.

//in bashphp artisan make:controller FilterController

Let’s go in routes/web.php:

//in web.phpRoute::get('/ads/index/filters', [FilterController::class, 'filters'])->name('filters');

In FilterController:

//in FilterControllerpublic function filters(Request $request){
dd($request->all());
}

For now, let’s limit ourselves with a Dump&Die function, just to verify the correctness of our code.

//within the form<form id="filters" action="{{route('filters')}}" method="GET" >

@csrf
<select name="categoryId" id="categoryId" onchange="document.querySelector('#filters').submit();">
<option value="" > - </option>
@foreach ($categories as $category)
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</select></form>
The result of the code above: a classic <select> tag to show our categories

It is the onchange attribute that actually trigger the form: we are practically telling it “when this element changes, submit the DOM element with id #filters” (the form itself).

Let’s test our request, thanks to the dd previously placed in the FilterController.

If so, let’s now try to add the filter for sorting by creation date:

//inside the component   <input type="radio" id="dateDescendantOrder" name="order" value="descendantDateOrder" onclick="document.querySelector('#filters').submit();">
<label for="dateDescendantOrder">Most recent</label>
<input type="radio" id="dateAscendantOrder" name="order" value="ascendantDateOrder" onclick="document.querySelector('#filters').submit();">
<label for="dateAscendantOrder">Less recent</label>
The result of the code above: a classic radio button for most recent and less recent

The onclick attribute works just like onchange, however submitting the form a the click on the radio button instead of at the changing of the category.

Another test of the request is in order.

Now that everything comes to us correctly in the Request, we need to set up the queries:

//inside FilterController, inside filters( )public funtion filters(Request $request){
$query = Ad::query();

$query->when($request->categoryId, function ($q, $categoryId) {
return $q->where('category_id', $request);
})->when($request->order == 'descendantDateOrder', function($q) {
return $q->orderBy('created_at', 'DESC');
})->when($request->order == 'ascendantDateOrder', function($q){
return $q->orderBy('created_at', 'ASC');
$ads = $query->get(); return view('ad.index', compact('ads'));}

Let’s explore the function written above:
starting from the beginning, whenever we interrogate a model in Eloquent (Ad, in our case), we are using the Eloquent Query Builder. Eloquent models pass calls to the Query Builder using various magic methods: these calls are returned by the query() function.

Therefore, we are now saving in $query all the information relating to the various calls to the Query Builder and, thanks to it, we have the possibility of nesting different queries, one after the other: to do so, we must use a Eloquent method called when().

The when() method accepts two input parameters: the condition for the query to be launched (as if it were an if) and a callback function. The callback function then accepts several other parameters:

  • necessary is a support variable indicating the queries (in our case $q);
  • it can also accept as a parameter the value of the condition, by inserting a new formal parameter (in our case $categoryId);
  • If we actually wanted to use the entire $request object as well, we will have to insert, after the keyword function, the keyword use($request);
//sintax example for the above statement$query->when($request->categoryId, function ($q, $categoryId) use ($request) {
...code...
});

By nesting the various queries with the when() method, we have the ability to launch our form several times, while saving the various data over time within the Query Builder.

As we go along with the requests, a series of queries is formed in $query, from which we can get a collection through the get() method.

We will then save this collection in $ads, which we will then return to the view for the user to see.

We can see, however, that any type of visual feedback for the user is missing. We need to do a few steps first:

//in FilterController, inside filters( )   
...code...
$ads = $query->get(); $filters = $request->all(); return view('ad.index', compact('ads', 'filters'));}

In $filters we will save the request, so to have all filters chosen by the user and make them available for checking the inputs given.

We have now the necessity to update our filter component as it will have to accept the list of checked filters:

//in Filter.phpclass Filter extends Component {   public $categories;
public $filters;
public function __construct(Collection $categories, $filters){
$this->categories = $categories;
$this->filters = $filters;
}
...code...//in blade<x-filter :categories="$categories" :filters="$filters" />

Now, we have to manage the inputs:

//inside the component<input ...code... {{isset($filters['order']) && $filters['order'] == 'descendantDateOrder' ? 'checked' : ''}}>

What have we done above? If a given filter is present in $filters, then the relevant checkbox or radio button will have the checked attribute, giving visual feedback to the user.

Congratulations! You successfully implemented filters in your project!

- Conclusions

As mentioned at the beginning, filters are a very common feature in today’s projects and we have lots of tools to implement them.

JavaScript is often required because it allows us to effectively use query strings to filter researches.

Example of query string usage on Amazon

But by using Laravel ORM to its full potential, we can achieve the same result, with a cleaner URI and without abusing selections.

Have you tried using this method in any of your projects? Let me know how it worked or if I can improve something! Looking forward to your feedback!

--

--

Davide Cariola

Backend and Laravel Specialist @ Aulab | Scrum Fundamentals Certified™ — follow me at davidecariola.it