plin-code/laravel-full-name
plin-code/laravel-full-name is a Laravel package that adds a reusable FullName trait/accessor to models, combining first, middle, and last name fields into a normalized full name for display, search, and sorting. Configurable name parts and separators keep Eloquent models simple.
This is a small, focused package: it centralises the "sort by a person's full name" problem, including the awkward case where the name lives on a related model. v1.1.1 is a maintenance release (PHPStan docblock narrowing + a Rector cleanup), so installation steps are unchanged.
composer require <vendor>/<package> — no service provider or config publishing appears necessary for the core matcher.src/FullNameOptions.php — the immutable options object (which columns make up the name, whether a relation is traversed).src/FullNameMatcher.php — applySort() and applySortWithRelation().users index sorted by first_name + last_name:use Vendor\Package\FullNameOptions;
use Vendor\Package\FullNameMatcher;
$options = FullNameOptions::make()
->withColumns(['first_name', 'last_name']);
return User::query()
->tap(fn ($q) => FullNameMatcher::make($options)->applySort($q, 'asc'))
->paginate();
If your first task is "sort the posts list by author name", skip straight to applySortWithRelation() — that is the reason most people install this package.
Normalise direction at the boundary. applySort() narrows $direction to 'asc'|'desc', so let validation do the narrowing rather than casting:
$request->validate([
'direction' => ['nullable', Rule::in(['asc', 'desc'])],
]);
$direction = $request->string('direction')->toString() ?: 'asc';
Wrap it in a model scope so controllers stay free of package imports:
public function scopeSortByFullName(Builder $query, string $direction = 'asc'): Builder
{
return FullNameMatcher::make(static::fullNameOptions())
->applySort($query, $direction);
}
Expose fullNameOptions() as a static factory on the model — it keeps the column list (first_name, last_name, prefixes, middle names) in one place and makes it easy to override per model.
Sorting across a relation (posts → author) is the pattern to build around:
FullNameMatcher::make(FullNameOptions::make()->withRelation('author'))
->applySortWithRelation($query, $direction);
Keep withRelation() explicit — v1.1.1 removed the redundant relation: null argument from withoutRelation(), confirming the options object is meant to be configured positively rather than by nulling things out.
Compose with existing query state (where filters, withCount, with() eager loads) by applying the sort last; the matcher should not be the thing deciding pagination or select columns.
Test by asserting order, not SQL. assertSeeInOrder() on the rendered index, or pluck('id')->all() against an expected array, survives internal changes like the v1.1.1 docblock/Rector edits. If you do snapshot toSql(), expect churn.
Static analysis in CI. Run PHPStan/Larastan at the level the package itself uses; the v1.1.1 fix exists specifically because Laravel 13 tightened orderBy()'s signature, so your own call sites benefit from the same narrowing.
'asc'|'desc'. Passing 'ASC', 'ascending', or a raw ?string from the request will now surface as a PHPStan error (and possibly a runtime orderBy complaint on Laravel 13). Validate with Rule::in() and default to 'asc'.applySortWithRelation() will join the related table, so:
select()/where() calls to avoid ambiguous-column errors (the related table almost certainly has its own id, created_at, name).distinct() or constraining the relation.CONCAT is unindexable. Even with correct indexes, MySQL/Postgres generally cannot use an index for ORDER BY CONCAT(first_name, ' ', last_name). For large tables, add a generated/stored full_name column with an index and keep the package for the relation-traversal case.NULL last_name makes CONCAT return NULL in MySQL, dumping those rows at one end of the sort with no obvious cause. Prefer COALESCE(last_name, '') in your column config if the package allows raw expressions; otherwise clean the data.Zoe vs zoe and accented names will order differently between MySQL (utf8mb4_unicode_ci) and Postgres (C). Pin collation in tests if order assertions are flaky.relation: null removal, docblock narrowing) are exactly the kind of tidy-ups you would miss in a vendored copy. Call the public API.->toSql() plus ->getBindings() on the builder after applying the sort is the fastest way to see the generated join and ORDER BY. Wrap it in a DB::listen() in a local service provider if you want it logged for every request.FullNameOptions is where you customise columns and relation; if you need a different sort expression entirely, write a plain orderByRaw() scope instead of fighting the matcher.^1.1) and let patch releases like this one come in automatically — they are static-analysis and internal cleanups with no behaviour change.How can I help you explore Laravel packages today?