Looking to hire Laravel developers? Try LaraJobs

laravel-searchable maintained by mozex

Description
Advanced multi-column, relation, and morph search for Laravel Eloquent models
Author
Last update
2026/04/16 20:32 (dev-main)
License
Downloads
1

Comments
comments powered by Disqus

Laravel Searchable

Latest Version on Packagist GitHub Tests Action Status Docs License Total Downloads

Add a Searchable trait to any Eloquent model and search across multiple columns, regular relations, polymorphic relations, and even cross-database relations with a single ->search() call. Works alongside Laravel Scout. Ships with optional Filament integration for table search and global search.

Read the full documentation at mozex.dev: searchable docs, version requirements, detailed changelog, and more.

Table of Contents

Support This Project

I maintain this package along with several other open-source PHP packages used by thousands of developers every day.

If my packages save you time or help your business, consider sponsoring my work on GitHub Sponsors. Your support lets me keep these packages updated, respond to issues quickly, and ship new features.

Business sponsors get logo placement in package READMEs. See sponsorship tiers →

Installation

Requires PHP 8.2+ - see all version requirements

composer require mozex/laravel-searchable

That's it. No config files to publish, no migrations to run.

Basic Usage

Add the Searchable trait to your model and define which columns should be searchable. You can mix direct columns, relation columns, and morph relations in the same array:

use Mozex\Searchable\Searchable;

class Comment extends Model
{
    use Searchable;

    public function searchableColumns(): array
    {
        return [
            'body',                          // direct column
            'author.name',                   // BelongsTo relation
            'tags.name',                     // HasMany relation
            'commentable:post.title',        // morph relation
            'commentable:video.name',        // another morph type
        ];
    }

    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}

Then search:

// Shortest form, searches all configured columns
Comment::search('laravel')->get();

// Chain with other query constraints
Comment::query()
    ->where('published', true)
    ->search($request->input('q'))
    ->paginate();

The search wraps all its conditions in a WHERE (... OR ...) group, so it plays nicely with any existing query constraints.

Search Types

Direct Columns

Plain column names on the model's own table:

public function searchableColumns(): array
{
    return ['title', 'body', 'slug'];
}

Relation Columns

Use dot notation to search through BelongsTo and HasMany relations:

public function searchableColumns(): array
{
    return [
        'title',
        'author.name',      // BelongsTo
        'author.email',     // BelongsTo, different column
        'comments.body',    // HasMany
        'tags.name',        // BelongsToMany / HasMany
    ];
}

Morph Relations

For polymorphic relations, use relation:morphType.column notation. The morph type needs to match your morph map alias:

// In a ServiceProvider:
Relation::morphMap([
    'post' => Post::class,
    'video' => Video::class,
]);
class Comment extends Model
{
    use Searchable;

    public function searchableColumns(): array
    {
        return [
            'body',
            'commentable:post.title',        // search Post's title
            'commentable:video.name',         // search Video's name
            'commentable:post.author.name',   // nested: Post -> Author -> name
        ];
    }

    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}

Nested relations inside morph targets work too. commentable:post.author.name first resolves the morph to a Post, then follows the author relation on Post to search the author's name.

Cross-Database Relations

If a BelongsTo relation points to a model on a different database connection, the package picks this up on its own. Since cross-database JOINs aren't possible, it runs a separate query on the external connection, fetches matching IDs (capped at 50), and uses whereIn on the foreign key. Nothing to configure.

Morph relations to external connections work the same way.

Case Sensitivity

Searches are case-insensitive by default. Comment::search('LARAVEL') matches rows containing laravel, Laravel, or LARAVEL without any extra flag or argument.

This comes from Laravel's whereLike helper, which the package uses for every search type (direct, relation, morph, and external). Actual behavior follows your database:

  • MySQL / MariaDB: case-insensitive on the usual _ci collations like utf8mb4_unicode_ci and utf8mb4_0900_ai_ci. A column on a _bin or _cs collation matches case-sensitively.
  • PostgreSQL: always case-insensitive. whereLike compiles to ILIKE.
  • SQLite: case-insensitive for ASCII only. Café and café won't match each other under the default LIKE.

The package doesn't expose a flag to flip this. If you need case-sensitive matching on a normally case-insensitive column, change the column's collation at the database level.

Column Filtering

You can override or adjust which columns are searched per-query:

// Search only specific columns (ignores searchableColumns)
Post::search('term', in: ['title', 'body'])->get();

// Add extra columns on top of searchableColumns
Post::search('term', include: ['slug'])->get();

// Exclude specific columns from searchableColumns
Post::search('term', except: ['author.name'])->get();

// Combine them
Post::search('term', include: ['slug'], except: ['body'])->get();

All three parameters accept a string or an array.

Filament Integration

When Filament is installed, the package registers an advancedSearchable() macro on TextColumn. Add it to one column in your table, and it'll search across all your model's configured searchable columns:

use Filament\Tables\Columns\TextColumn;

TextColumn::make('title')
    ->advancedSearchable()
    ->sortable(),

You can pass the same in, include, except parameters:

TextColumn::make('title')
    ->advancedSearchable(except: ['author.name'])
    ->sortable(),

Global Search

Register the provider on your panel:

use Mozex\Searchable\Filament\SearchableGlobalSearchProvider;

return $panel
    ->id('admin')
    ->path('admin')
    ->globalSearch(SearchableGlobalSearchProvider::class);

Then on each resource, define getGloballySearchableAttributes() to control which columns global search uses for that resource. Return all of the model's columns, or a subset:

class CourseResource extends Resource
{
    // Use everything the model declared as searchable
    public static function getGloballySearchableAttributes(): array
    {
        return (new Course)->searchableColumns();
    }
}

class PostResource extends Resource
{
    // Or limit global search to a subset, even though
    // the Post model has more columns in searchableColumns()
    public static function getGloballySearchableAttributes(): array
    {
        return ['title', 'author.name'];
    }
}

Each resource you want in global search needs to define getGloballySearchableAttributes(). Resources without it are excluded from global search entirely.

Resources whose models don't use the Searchable trait fall through to Filament's default global search behavior.

Handling Conflicts

Laravel Scout

Scout and this package both expose a search() method on your model. Scout's is a static method that hits its search engine; this package's is a query scope that runs SQL. Technically, different call paths, so they don't collide.

In practice, having two search entry points on the same model gets confusing fast. The cleaner approach is to alias this package's scope to a different name using PHP's trait aliasing, so each search path has its own clear name:

use Laravel\Scout\Searchable;
use Mozex\Searchable\Searchable as DatabaseSearchable;

class Lesson extends Model
{
    use DatabaseSearchable {
        scopeSearch as scopeDatabaseSearch;
    }
    use Searchable;

    public function searchableColumns(): array
    {
        return ['name', 'description'];
    }
}

Now Lesson::search('term') runs Scout's full-text search, and Lesson::databaseSearch('term') runs this package's database search. No ambiguity.

For the Filament macro, pass the renamed method:

TextColumn::make('name')->advancedSearchable(method: 'databaseSearch')

Existing search Methods

Sometimes you can't reach this package's scope through $query->search() because something else already owns that name. Two common cases:

  • A custom Eloquent Builder defines its own search() (Corcel's PostBuilder is the textbook example). $query->search() calls the Builder's method, not this package's scope.
  • A parent model you extend already declares scopeSearch with a different signature. Adding our trait causes a fatal error because PHP enforces method signature compatibility between trait methods and inherited methods.

For both cases, use applySearch() to invoke the scope directly without going through the search name:

$query = Product::query();
$query->getModel()->applySearch($query, 'term');
$results = $query->get();

applySearch accepts the same parameters as the scope:

$query->getModel()->applySearch($query, 'term', in: ['title', 'body']);
$query->getModel()->applySearch($query, 'term', except: ['author.name']);

If a parent model's scopeSearch signature conflicts with this package's, alias our scope to a different name when adding the trait (the same pattern as the Scout case above):

use Mozex\Searchable\Searchable as DatabaseSearchable;

class Product extends VendorModel
{
    use DatabaseSearchable {
        scopeSearch as scopeDatabaseSearch;
    }
}

For the Builder case specifically, you can also override the Builder's search() to delegate back to applySearch, so the rest of your codebase keeps calling $query->search():

class ProductBuilder extends \Corcel\Model\Builder\PostBuilder
{
    public function search($term = false, ...$args): self
    {
        $query = Product::query();

        (new Product)->applySearch($query, $term, ...$args);

        return $query;
    }
}

Resources

Visit the documentation site for searchable docs auto-updated from this repository.

  • AI Integration: Use this package with AI coding assistants via Context7 and Laravel Boost
  • Requirements: PHP, Laravel, and dependency versions
  • Changelog: Release history with linked pull requests and diffs
  • Contributing: Development setup, code quality, and PR guidelines
  • Questions & Issues: Bug reports, feature requests, and help
  • Security: Report vulnerabilities directly via email

License

The MIT License (MIT). Please see License File for more information.