Back to Course |
Laravel 11 Eloquent: Expert Level

Filter and Search Packages

I want to demonstrate a set of packages related to filtering the data and searching through it, with Eloquent.

First, let's see the packages that will help you run Eloquent queries flexibly, providing the parameters by your users.

It reminds me of the logic of GraphQL. So, GraphQL provides the schema, and then the client of GraphQL tells them what they need, what data, ordering, filters, and other parameters they need.


Spatie: Laravel Query Builder

So a package from Spatie called Laravel-query-builder may transform /users?filter[name]=John URL into an Eloquent query. The user in the URL provides that they need to filter names by John, and the syntax of your query would be:

use Spatie\QueryBuilder\QueryBuilder;
 
$users = QueryBuilder::for(User::class)
->allowedFilters('name')
->get();

Instead of doing User::where('name', 'LIKE', '%John%'), you allow the filter by name. Also, the package allows you to include the relationships automatically and provide the sorting.

In the database, I have eight users and made a query to allow filtering by name and ID and sorting by ID.

$users = QueryBuilder::for(User::class)
->allowedFilters(['name', AllowedFilter::exact('id')])
->allowedSorts('id')
->get();
 
foreach ($users as $user) {
print '<div><strong>' . $user->id . ': ' . $user->name . '</strong>: ' . $user->email . '</div>';
print '<hr />';
}

Without any filter or sorting applied, this is the result in the browser.

For example, let's filter by name for the dr keyword. The result is two users who have the dr keyword in their name.

The Exception will be thrown if you try to filter by anything that hasn't been added to the filters list.

If you don't want to throw Exception, you can set disable_invalid_filter_query_exception to true in the package config file.

Next, I added the exact filter for the ID. When exact is used, if you filter with the ID of one, then the query won't match records with the ID of 10, 11, etc.

And for sorting, if you provide only the column, the sorting will be in ascending order. But, if you add a minus before the sorting, it will be in descending order.

These are the options of the package by spatie Laravel-query-builder if you want to provide your users the possibility to filter their data themselves.


Tucker Eric: Eloquent Filter

An alternative package is called Tucker-Eric/EloquentFilter. By default, filters are in the app\ModelFilters folder, which can be changed in the packages config file namespace key.

The EloquentFilter\Filterable trait must be added to the Model. This trait will give access to the filter() method.

Model filters can be created using an artisan command.

php artisan model:filter User

Then, a method with a name filter to be used as a query parameter must be created in the created filter class.

app/ModelFilters/UserFilter.php:

class UserFilter extends ModelFilter
{
public $relations = [];
 
public function name(string $name): self
{
return $this->where('name', 'LIKE', '%' . $name . '%');
}
}

When querying the Model using the filter() method, a request can be passed, and the filter will just work.

$users = User::filter(request()->all())->get();
 
foreach ($users as $user) {
print '<div><strong>' . $user->id . ': ' . $user->name . '</strong>: ' . $user->email . '</div>';
print '<hr />';
}


You can check a video review of three packages for filtering and sorting on YouTube.

Now, let's take a look at a few packages for searching in Eloquent.


Spatie Laravel Searchable

Next, two packages allow you to search multiple Models simultaneously. The first package is spatie/laravel-searchable. After installing the package via composer, you must implement the Searchable interface on the Models. In this example, I will search for two Models: User by name, and Task by description columns.

So, I have implemented the Searchable interface in both Models and added the getSearchResult() public method.

app/Models/User.php:

use Spatie\Searchable\Searchable;
use Spatie\Searchable\SearchResult;
 
class User extends Authenticatable implements Searchable
{
// ...
 
public function getSearchResult(): SearchResult
{
return new SearchResult($this, $this->name);
}
}

app/Models/Task.php:

use Spatie\Searchable\Searchable;
use Spatie\Searchable\SearchResult;
 
class Task extends Model implements Searchable
{
// ...
 
public function getSearchResult(): SearchResult
{
return new SearchResult($this, $this->description);
}
}

Then, a simple SearchController was made to perform the search on two Models. The search keyword comes from a GET request query parameter.

app/Http/Controllers/SearchController.php:

use App\Models\User;
use App\Models\Task;
use Illuminate\Http\Request;
use Spatie\Searchable\Search;
 
class SearchController extends Controller
{
public function __invoke(Request $request)
{
$results = (new Search())
->registerModel(User::class, ['name'])
->registerModel(Task::class, ['description'])
->search($request->get('query'));
 
return view('search', compact('results'));
}
}

In the View, search results are displayed as a simple list.

resources/views/search.blade.php:

@foreach($results->groupByType() as $type => $modelSearchResults)
<h2>{{ $type }}</h2>
 
<ul>
@foreach($modelSearchResults as $modelSearch)
<li>{{ $modelSearch->title }}</li>
@endforeach
</ul>
@endforeach

In the database, I have seeded some data.

After trying to search for some keywords, I got the results from both Models.

The search is performed case insensitive.

For more, read the official documentation.


Protone Media: Cross Eloquent Search

The second package for searching in more than one Model at once is protonemedia/laravel-cross-eloquent-search.

This package only works with MySQL 8.0+

I have the same two Models, User and Task. After installing the package, you can use it immediately.

app/Http/Controllers/SearchController.php:

use App\Models\User;
use App\Models\Task;
use Illuminate\Http\Request;
use ProtoneMedia\LaravelCrossEloquentSearch\Search;
 
class SearchController extends Controller
{
public function __invoke(Request $request)
{
$results = Search::new()
->add(User::class, 'name')
->add(Task::class, 'description')
->beginWithWildcard()
->search($request->get('query'))
->groupBy(fn($item) => class_basename($item));
 
return view('search', compact('results'));
}
}

I added the groupBy() to group by a Model name. After grouping, I have such results:

array:2 [▼
"User" => Illuminate\Database\Eloquent\Collection {#1273 ▼
#items: array:2 [▶]
#escapeWhenCastingToString: false
}
"Task" => Illuminate\Database\Eloquent\Collection {#1274 ▼
#items: array:1 [▶]
#escapeWhenCastingToString: false
}
]

This way, when doing a foreach loop in the View, I can easily get the Model name.

resources/views/search.blade.php:

@foreach($results as $key => $modelSearchResults)
<h2>{{ $key }}</h2>
 
<ul>
@foreach($modelSearchResults as $searchResult)
@if($key === 'User')
<li>{{ $searchResult->name }}</li>
@elseif($key === 'Task')
<li>{{ $searchResult->description }}</li>
@endif
@endforeach
</ul>
@endforeach

Instead of doing the if checks, there are better ways to show results, for example, using Blade Dynamic Components.

After making the search result look like this:

For more, read the official documentation.