Skip to content

Instantly share code, notes, and snippets.

@radmen
Last active December 11, 2020 12:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save radmen/65528d9a8fb157b7fe6fb9dd4c645981 to your computer and use it in GitHub Desktop.
Save radmen/65528d9a8fb157b7fe6fb9dd4c645981 to your computer and use it in GitHub Desktop.
Eloquent search scope

Little code snippet inspired by Freek and others. Most likely, almost a copy of their implementation. I'm using this a lot in recent projects, so I'd like to keep it easily accessible.

Scope supports:

  • searching using relations
  • (optional) breaking the word by space and matching to all parts

How it works

  • for each field defined in $searchable attribute scope will add LIKE query
  • fields are joined using orWhere - if there's a match in at least one field, the model will be fetched
  • you can add relations to the $searchable

Installation

  • copy the Search.php file to app/Models/Scopes/

  • add the trait to the model:

    <?php
    
    namespace App\Models;
    
    class Foo extends \Illuminate\Database\Eloquent\Model
    {
      use Scopes\Search;
    }
  • define $searchable private attributes with list of fields used in search

      protected $searchable = ['name', 'email'];
  • (optional) set custom options

      protected $searchOptions = ['break_words' => true];
<?php
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
trait Search
{
protected array $defaultSearchOptions = [
'break_words' => false,
];
public function scopeSearch(Builder $query, ?string $search)
{
$searchTerm = trim($search ?? '');
$options = array_merge(
$this->defaultSearchOptions,
$this->searchOptions ?? [],
);
if (! $searchTerm || mb_strlen($searchTerm) < 3) {
return;
}
$searchWords = $this->breakToWords($searchTerm, $options);
$searchable = $this->searchable ?? [];
[$relations, $fields] = collect($searchable)
->partition(fn ($field) => Str::contains($field, '.'));
$groupedRelations = $relations
->groupBy(
fn ($relation) => preg_replace('/\.\w+?$/', '', $relation)
)
->map(fn ($list) => $list->map(
fn ($relation) => preg_replace('/^.*\.(\w+)$/', '$1', $relation)
));
$applyWords = function ($field, $searchWords) {
return fn ($innerQuery) => $searchWords->each(
fn ($word) => $innerQuery->where($field, 'LIKE', $word)
);
};
$query->where(function (Builder $query) use ($searchWords, $fields, $groupedRelations, $applyWords) {
$fields->each(fn ($field) => $query->orWhere($applyWords($field, $searchWords)));
$groupedRelations->each(function ($fields, $relation) use ($searchWords, $query, $applyWords) {
$query->orWhereHas($relation, function ($query) use ($fields, $searchWords, $applyWords) {
$query->where(function ($query) use ($fields, $searchWords, $applyWords) {
$fields->each(fn ($field) => $query->orWhere($applyWords($field, $searchWords)));
});
});
});
});
}
private function breakToWords($searchTerm, array $options): Collection
{
if (! $options['break_words']) {
return collect(["%{$searchTerm}%"]);
}
return collect(preg_split('/\W+/', $searchTerm))
->map(fn ($value) => trim($value))
->reject(fn ($value) => mb_strlen($value) < 3)
->map(fn ($value) => "%{$value}%");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment