Search and sort Eloquent queries (and Filament tables) by a person's full name stored across two columns (first_name and last_name), either on the main model or on a BelongsTo / HasOne relation.
Filament's built-in ->searchable(['first_name', 'last_name']) searches each column independently, which fails for composite queries like "mario rossi". The native ->sortable(query: ...) works for direct columns but requires repetitive custom join logic for relation-based sort. This package encapsulates the solution once, tested once, documented once.
composer require plin-code/laravel-full-nameNo config file, no migrations, no Blade views, no Artisan command. The service provider auto-registers.
- PHP 8.4
- Laravel 12 or 13
- Filament 4 or 5 (optional, only needed for the Filament layer)
- MySQL 8, PostgreSQL 14+, or SQLite 3
Booking::query()
->searchFullName($request->input('q'))
->orderByFullName('asc')
->paginate();
Booking::query()
->searchFullName($request->input('q'), relation: 'user')
->orderByFullName('asc', relation: 'user')
->paginate();use Filament\Tables\Columns\TextColumn;
TextColumn::make('full_name')
->fullNameSearchable()
->fullNameSortable();TextColumn::make('user.full_name')
->fullNameSearchable(relation: 'user')
->fullNameSortable(relation: 'user');// User hasOne(Hiker::class) — search and sort users by their hiker full name.
TextColumn::make('hiker.full_name')
->fullNameSearchable(relation: 'hiker')
->fullNameSortable(relation: 'hiker');TextColumn::make('full_name')
->fullNameSearchable(
firstNameColumn: 'given_name',
lastNameColumn: 'family_name',
)
->fullNameSortable(
firstNameColumn: 'given_name',
lastNameColumn: 'family_name',
);The complete API surface lives in docs/api.md.
The matching strategy uses LOWER(CONCAT(COALESCE(first, ''), ' ', COALESCE(last, ''))) which prevents btree indexes from being used on first_name or last_name. On tables up to a few hundred thousand rows this is typically acceptable for admin panel search. For very large tables, pair this package with a dedicated search engine (Meilisearch, Scout, Algolia) and use this package only for sort.
The core uses LOWER(CONCAT(COALESCE(first, ''), ' ', COALESCE(last, ''))) matched with LIKE ? ESCAPE '!' in both forward and reversed concatenation forms.
| Query | Record | Matches |
|---|---|---|
mario |
first_name='Mario', last_name='Rossi' |
yes |
rossi |
first_name='Mario', last_name='Rossi' |
yes |
mario rossi |
first_name='Mario', last_name='Rossi' |
yes |
rossi mario |
first_name='Mario', last_name='Rossi' |
yes |
maria |
first_name='Marianna', last_name='Rossi' |
yes (substring, single token) |
maria rossi |
first_name='Marianna', last_name='Rossi' |
no (multi token) |
marianna rossi |
first_name='Marianna', last_name='Rossi' |
yes |
mario giovanni rossi |
first_name='Mario Giovanni', last_name='Rossi' |
yes |
rossi mario giovanni |
first_name='Mario Giovanni', last_name='Rossi' |
yes |
bianchi mario |
first_name='Mario', last_name='Rossi Bianchi' |
yes |
The asymmetry between single token and multi token queries is intentional and emerges from the SQL pattern. Single token queries use substring match, so maria matches records containing maria anywhere in either column. Multi token queries require the tokens to appear contiguously with the separating space between them in the concatenated first last or last first form, so maria rossi matches Maria Rossi but not Marianna Rossi (the separator space is not present between maria and rossi in the concatenation). Single token queries are exploratory (the user may be typing a prefix), multi token queries target a specific person.
See docs/conventions.md for the rationale behind the naming split between the Eloquent and Filament layers.
- Only the
BelongsToandHasOnerelation types are supported.HasMany,BelongsToMany,MorphTo, and nested relations raiseUnsupportedRelationExceptionat query build time. - Accent and diacritic normalization is delegated to the database collation. On MySQL, use
utf8mb4_unicode_ciorutf8mb4_0900_ai_ci. On PostgreSQL, consider theunaccentextension if needed. - Fuzzy matching (soundex, metaphone, Levenshtein, trigram) is out of scope.
- Single column full name (one
namecolumn) is not handled. Filament's native->searchable(['name'])covers that case already. - Empty or whitespace only search input leaves the query unchanged (no
WHEREclause is added). - When combining
orderByFullName(relation: ...)with an explicit->select([...])on the main query, qualify the column names with the main table name (for example->select(['test_bookings.id'])rather than->select(['id'])). The package performs ajoinSubunder the hood, which can introduce ambiguity for unqualified columns that exist on both tables.
composer test
composer analyse
composer formatPlease see CONTRIBUTING for details.
Please see CHANGELOG for recent changes.
The MIT License (MIT). Please see License File for more information.