# Laravel Searchable

**Package:** Laravel Searchable | **Version:** 1 | **URL:** https://mozex.dev/docs/laravel-searchable/v1

---

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.

## Installation

> **Requires [PHP 8.2+](https://php.net/releases/)** - see [all version requirements](https://mozex.dev/docs/laravel-searchable/v1/requirements)

```bash
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:

```php
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:

```php
// 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:

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

### Relation Columns

Use dot notation to search through BelongsTo and HasMany relations:

```php
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:

```php
// In a ServiceProvider:
Relation::morphMap([
    'post' => Post::class,
    'video' => Video::class,
]);
```

```php
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:

```php
// 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:

```php
use Filament\Tables\Columns\TextColumn;

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

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

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

### Global Search

Register the provider on your panel:

```php
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:

```php
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:

```php
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:

```php
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:

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

`applySearch` accepts the same parameters as the scope:

```php
$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):

```php
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()`:

```php
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;
    }
}
```

---

## Table of Contents

- [Quick Start](https://mozex.dev/docs/laravel-searchable/v1)
- [AI Integration](https://mozex.dev/docs/laravel-searchable/v1/ai-integration)
- [Support Us](https://mozex.dev/docs/laravel-searchable/v1/support-us)
- [Requirements](https://mozex.dev/docs/laravel-searchable/v1/requirements)
- [Changelog](https://mozex.dev/docs/laravel-searchable/v1/changelog)
- [Contributing](https://mozex.dev/docs/laravel-searchable/v1/contributing)
- [Questions & Issues](https://mozex.dev/docs/laravel-searchable/v1/questions-and-issues)
- [About Mozex](https://mozex.dev/docs/laravel-searchable/v1/about)