Envision the need to implement a user search functionality. With Eloquent, you can execute the search in the following manner:
User::query() ->where('name', 'LIKE', "%{$searchTerm}%") ->orWhere('email', 'LIKE', "%{$searchTerm}%") ->get();
This will return all records that have a name or email that contains the string in $searchTerm
. If you’re using MySQL this search will also be perform in a case insensitive way, which is probably what your want.
Now, if you want to add a search, not only the User
model but to every model, you can add macro to the Eloquent’s Query builder.
Here’s how our macro could look like. You can put it the boot
method of App\Providers\AppServiceProvider
or a service provider of your own.
use Illuminate\Database\Eloquent\Builder; // ... Builder::macro('whereLike', function(string $attribute, string $searchTerm) { return $this->orWhere($attribute, 'LIKE', "%{$searchTerm}%"); });
We can now search our model like this:
User::query() ->whereLike('name', $searchTerm) ->whereLike('email', $searchTerm) ->get();
There’s still room for improvement. I don’t like the fact that we now have to repeat that whereLike
call for every attribute we want to search. Let’s fix that. Here’s an improved version of our macro.
Builder::macro('whereLike', function($attributes, string $searchTerm) { foreach(array_wrap($attributes) as $attribute) { $this->orWhere($attribute, 'LIKE', "%{$searchTerm}%"); } return $this; });
array_wrap
is a nice little Laravel helper. When given an array it just returns that array. When given something else it will wrap it in an array. So you know the result will always be an array.
The macro above can be used like this:
// searching a single column User::whereLike('name', $searchTerm)->get(); // searching multiple columns in one go User::whereLike(['name', 'email'], $searchTerm)->get();
Our macro already looks pretty good, but it has a nasty bug.
Consider this query:
User::query() ->where('role', 'admin') ->whereLike(['name', 'email'], 'john') ->get();
If you think this will return only users with an admin
role you’re mistaken. Because our whereLike
macro contains orWhere
this will return each user with an admin
role and all users whose name or email contain john
.
Let’s fix that by wrapping our orWhere
s in a function. This is the equivalent of setting brackets in the search query.
Builder::macro('whereLike', function ($attributes, string $searchTerm) { $this->where(function (Builder $query) use ($attributes, $searchTerm) { foreach (array_wrap($attributes) as $attribute) { $query->orWhere($attribute, 'LIKE', "%{$searchTerm}%"); } }); return $this; });
Now the query above will return all admins whose name or email contain john
.
As it stand we can now only search the attributes of the model where using the scope on. Let’s add support for searching the attributes of the relations of that model as well.
Builder::macro('whereLike', function ($attributes, string $searchTerm) { $this->where(function (Builder $query) use ($attributes, $searchTerm) { foreach (array_wrap($attributes) as $attribute) { $query->when( str_contains($attribute, '.'), function (Builder $query) use ($attribute, $searchTerm) { [$relationName, $relationAttribute] = explode('.', $attribute); $query->orWhereHas($relationName, function (Builder $query) use ($relationAttribute, $searchTerm) { $query->where($relationAttribute, 'LIKE', "%{$searchTerm}%"); }); }, function (Builder $query) use ($attribute, $searchTerm) { $query->orWhere($attribute, 'LIKE', "%{$searchTerm}%"); } ); } }); return $this; });
With that macro can do something like this:
Post::whereLike(['name', 'text', 'author.name', 'tags.name'], $searchTerm)->g