davidgorges/human-name-parser
Parse human names into structured parts with this PHP library. Split full names into salutation, first name, middle name, last name, and suffix, handling initials, nicknames, and common formats. Lightweight and easy to drop into Laravel or any PHP project.
1. Install it
composer require davidgorges/human-name-parser
There is no service provider, no facade, no config file, and nothing to publish. Composer's autoloader is all the integration you get. If you're used to Laravel packages having a config/human-name.php to tweak, that does not exist here — configuration means writing your own wrapper.
2. Where to look first
Open vendor/davidgorges/human-name-parser/src/ and read the single HumanName class top to bottom (it's small). Do this before writing any code — check the exact namespace for the use statement, the public property/method names, and the built-in lists of titles and suffixes. That file is the documentation for edge-case behavior (what counts as a suffix, how "van" is handled, etc.).
3. First use case: one "full name" field → first/last
The canonical first task is a single name input (signup form, CSV import, CRM export) that your database wants split into first_name / last_name. In tinker:
use HumanNameParser\HumanName; // verify namespace after install
$name = new HumanName('Mr. John A. Kenneth Doe Jr.');
$name->title; // 'Mr.'
$name->first; // 'John'
$name->middle; // 'A. Kenneth'
$name->last; // 'Doe'
$name->suffix; // 'Jr.'
$name->leadingInitial; // for patterns like "J. Walter Weatherman"
(string) $name; // re-rendered full name, see Gotchas
The parsed parts are exposed as magic properties resolved from an internal parts array, and there's a method to reassemble the full name (getFullName()). Once you can reproduce the example above in tinker, you know the package is wired correctly and you're ready to put a wrapper around it.
Never make the parsed parts your only data. Store the user's original input alongside the derived columns so you can re-parse later, show exactly what they typed, and recover when the parser guesses wrong.
Schema::table('contacts', function (Blueprint $table) {
$table->string('full_name'); // source of truth
$table->string('first_name')->nullable();
$table->string('middle_name')->nullable();
$table->string('last_name')->nullable();
$table->string('name_suffix')->nullable();
$table->string('name_title')->nullable();
});
Because the package is a bare class, wrap it once so the rest of your app doesn't import HumanName directly. This gives you a single place to normalize input, handle nulls, and swap the library out if the 17-star dependency risk ever bites.
namespace App\Support;
use HumanNameParser\HumanName;
class NameParts
{
public function __construct(
public ?string $title = null,
public ?string $first = null,
public ?string $middle = null,
public ?string $last = null,
public ?string $suffix = null,
) {}
public static function fromString(?string $input): self
{
$input = trim(preg_replace('/\s+/u', ' ', (string) $input));
if ($input === '') {
return new self();
}
$parsed = new HumanName($input);
return new self(
title: $parsed->title ?: null,
first: $parsed->first ?: null,
middle: $parsed->middle ?: null,
last: $parsed->last ?: null,
suffix: $parsed->suffix ?: null,
);
}
}
Normalizing whitespace before parsing is the highest-value thing in that method — stray newlines and double spaces from pasted form data cause far more bad parses than exotic names do.
Two idiomatic fit points. An accessor is simplest:
protected function nameParts(): Attribute
{
return Attribute::get(fn () => NameParts::fromString($this->full_name));
}
Then anywhere: $contact->name_parts->last, {{ $contact->name_parts->first }}.
If you'd rather write to first_name/last_name automatically on save, a small custom cast or a saving hook on the model works:
protected static function booted(): void
{
static::saving(function (Contact $contact) {
if ($contact->isDirty('full_name')) {
$parts = NameParts::fromString($contact->full_name);
$contact->fill([
'first_name' => $parts->first,
'middle_name' => $parts->middle,
'last_name' => $parts->last,
'name_suffix' => $parts->suffix,
'name_title' => $parts->title,
]);
}
});
}
Guard on isDirty('full_name') so you don't re-parse (and potentially overwrite hand-corrected parts) on every unrelated save.
A very common workflow: accept one "Full name" field (better UX and better data than forcing users into a first/last split that doesn't fit their culture), then split server-side. You can cheaply measure confidence by re-rendering the name and comparing normalized versions:
$parts = NameParts::fromString($request->validated('full_name'));
$roundTrip = trim(implode(' ', array_filter([
$parts->title, $parts->first, $parts->middle, $parts->last, $parts->suffix,
])));
$confident = mb_strtolower(preg_replace('/\W+/u', '', $roundTrip))
=== mb_strtolower(preg_replace('/\W+/u', '', $request->validated('full_name')));
When $confident is false, flag the record for review rather than silently storing bad parts. Wrap this in a custom validation rule or a queued "data quality" job — there is no validation rule in the package.
For a migration of an existing users.name column, do it as an artisan command with chunking, not inline in the migration:
User::query()->whereNull('first_name')->chunkById(500, function ($users) {
foreach ($users as $user) {
$parts = NameParts::fromString($user->name);
$user->forceFill([
'first_name' => $parts->first,
'last_name' => $parts->last,
])->saveQuietly(); // skip observers/events during backfill
}
});
chunkById + saveQuietly() is the pattern; parsing cost per row is microseconds, so the bottleneck is your DB writes, not the library. Idempotency matters — constrain the query with whereNull('first_name') so a re-run doesn't clobber manually fixed records.
For an import job, parse once into your DTO and log the raw → parts mapping for a sample of rows (say the first 100) to a debug log. Imports are where you discover that 3% of your source data is "Doe, John" or "Dr. Smith" or a company name in the name field. Also add a name_parse_failed flag column so bad rows are visible in an admin screen instead of corrupting your mail-merge greetings.
Use the parts for formatting rather than string concatenation:
$greeting = $parts->first ? "Hi {$parts->first}," : 'Hello,';
$formal = trim("{$parts->title} {$parts->first} {$parts->last}");
Falling back to the raw string when first is null is a good habit — "Hi ," in an email is the classic bug.
Magic properties are silently forgiving. Accessing $name->firstName (camelCase) or a typo'd property returns null (or throws, depending on the exact class version) rather than erroring usefully. There's no IDE autocompletion and no static analysis help. Always go through your own DTO/wrapper with typed properties so PHPStan and your editor catch mistakes.
Ambiguity is inherent, not a bug. "Mary Ann Smith" is genuinely ambiguous — expected first/middle, but data entry frequently means a double first name. "Smith" alone usually lands in first, not last. Two-token names are split at the last space; three-plus tokens are split heuristically. There is no correct answer; the fix is a review/override UI, not a config option (there isn't one).
Name particles are a weak spot. "Juan de la Cruz", "van der Berg", "Al-Sayed" — expect last to fragment across middle/last. The package's rules are English-centric, and the title/suffix dictionaries are likewise English. If non-English names are a significant share of your data, budget time to test a fixture set of real names from your actual users before you ship, and consider only using the parser for the first token (which is far more reliable than last for greetings).
Comma forms and suffixes need verification. "Doe, John" and "Smith, Jr., John" rely on specific patterns; test them in tinker early. Also test III vs 3rd, Ph.D., O'Brien, Jean-Luc, and accented characters (José) — the regexes are not all u-flag aware, and this is the single most valuable 20 minutes you can spend.
Empty and null inputs. Passing null or '' to the constructor is easy to do from a nullable column; normalize to '' and short-circuit in your wrapper (as in the code above) rather than relying on the library's behavior.
Don't assume round-tripping is exact. Reassembling parts may normalize case, punctuation, or spacing, so getFullName() !== $original doesn't necessarily mean parsing failed. Compare on a normalized/loose basis (as in the confidence check) and only ever display the original string to the user.
Don't overwrite hand-corrected data. Once a user or admin fixes last_name, your parser must not stomp it on the next save. Either gate on isDirty('full_name') or keep a name_parts_locked boolean.
No config, no extension hooks — subclass if needed. There's nothing to publish and no driver interface. To customize behavior, extend the class (or, more practically, preprocess the string before constructing it: strip honorifics from a known list, collapse "Lastname, Firstname" into "Firstname Lastname", lowercase ALL-CAPS names). Preprocessing in your wrapper is easier to test and revert than a fork.
Maintenance risk is real. 17 stars, 0 dependents, a single author, and it's a port of an older algorithm. Pin the version in composer.json rather than using a loose constraint, and because it's MIT you can vendor or fork the single class into app/Support if upstream goes quiet — it's one file, which makes that a genuinely low-cost exit strategy. Don't let the library leak into more than one or two call sites (another reason for the wrapper).
Add your own tests immediately. Write a PHPUnit test with a data provider of your real names as they arrive from the forms, imports, and third parties you actually use. That file will document the parser's behavior for your domain far better than upstream docs, and it's what lets you upgrade or replace the package with confidence later.
How can I help you explore Laravel packages today?