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.
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.
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.
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.
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.