Looking to hire Laravel developers? Try LaraJobs

username maintained by x-laravel

Description
Configurable username validation, reserved-name checking and username generation for Laravel.
Last update
2026/10/14 21:41 (1.x-dev)
License
Links
Downloads
0

Comments
comments powered by Disqus

Username for Laravel

Username handling for Laravel in one place: a configurable format rule, a reserved-name rule and a generator that turns any text — a name, an e-mail prefix, a social nickname — into a free username that passes the format rule.

Requirements

Package version PHP Laravel
1.x 8.2+ 12, 13

Installation

composer require x-laravel/username

The service provider is registered through package discovery. Publish the config to change the defaults:

php artisan vendor:publish --tag=username-config

The format

Out of the box a username:

  • contains only lowercase English letters, digits, hyphens (-), underscores (_) and dots (.);
  • does not start or end with a dot and has no two dots in a row;
  • is 2 to 30 characters long.

Every part of it is a config key in config/username.php:

Key Default Meaning
rules.min 2 Minimum length
rules.max 30 Maximum length
rules.dots true Allow dots (never at the edges or doubled)
rules.hyphens true Allow hyphens
rules.underscores true Allow underscores
rules.uppercase false Allow uppercase letters
rules.numeric true Allow usernames made of digits only
fallback 'user' Base the generator uses when nothing usable is left of its input
reserved see the file Names the reserved-name rule rejects

Letters outside the English alphabet are never allowed, so a username always fits in a URL without encoding.

Usage

Format rule

use XLaravel\Username\Rules\Username;

$request->validate([
    'username' => ['required', new Username, 'unique:users'],
]);

The rule reads the config; override any part for a single field:

Username::make()->min(3)->max(20)->dots(false)->numeric(false)->uppercase();

With uppercase off the rule rejects capitals rather than lowering them. Lowercase the input before validation if users may type capitals, for example in a form request:

protected function prepareForValidation(): void
{
    $this->merge(['username' => strtolower((string) $this->input('username'))]);
}

The error message names exactly the characters that are allowed, so it stays correct when you change the config.

Reserved names

use XLaravel\Username\Rules\NotReserved;

$request->validate([
    'username' => ['required', new Username, new NotReserved(['acme']), 'unique:users'],
]);

The list comes from the reserved config key; names passed to the rule are added to it. Matching ignores case, dots, hyphens and underscores, so ad.min and Ad-Min count as admin.

Generating a username

use App\Models\User;
use Illuminate\Support\Str;
use XLaravel\Username\UsernameGenerator;

$username = UsernameGenerator::generate(
    Str::before($email, '@'),
    fn (string $candidate) => User::where('username', $candidate)->exists(),
);

generate() normalizes the text, then appends -2, -3, … until the candidate is long enough, not reserved and not taken — trimming the base so the result never exceeds the maximum length. The number is joined with a hyphen, or with an underscore when hyphens are off. The result always passes the format rule.

normalize() does the first half on its own: it transliterates to ASCII (Turkish letters such as ı, ş and ğ included), lowercases unless uppercase is allowed, turns runs of disallowed characters into a hyphen (an underscore when hyphens are off), collapses repeated dots, trims dots and separators from the edges and cuts the result to the maximum length.

UsernameGenerator::normalize('Iğdır Şükrü');      // igdir-sukru
UsernameGenerator::normalize('ahmet.can+promo');  // ahmet.can-promo

When digit-only usernames are not allowed, generate() puts the fallback in front of them, so 12345 becomes user12345.

Model trait

use XLaravel\Username\Concerns\HasUsername;

class User extends Model
{
    use HasUsername;

    public function usernameSource(): string
    {
        return $this->first_name . ' ' . $this->last_name;
    }
}

On every save the trait:

  • generates a free username from usernameSource() when the column is empty;
  • lowercases a username that was set or changed, unless uppercase is allowed.

A username that did not change is left alone, so existing rows are never rewritten behind your back.

The trait works on the username column. To use another one, set a property on the model:

protected string $usernameColumn = 'handle';

Override usernameColumn() instead when the column has to be decided at runtime.

Availability is checked with usernameTaken(), which ignores global scopes (soft-deleted rows still count) and the model's own row; override it to scope the check differently.

Two requests can pick the same free username at the same moment. When the trait generated the username and the insert then fails on the username's unique index, it generates a new one and tries again, up to three times. A username that was given rather than generated is never retried — the unique validation rule is the place for that. Inside an open transaction the insert runs in a savepoint, so a failed attempt does not break the surrounding transaction. The retry lives in the trait's save() method; if your model overrides save() itself, call parent::save() from it to keep the retry.

Reacting to a changed username

When a saved username changes, the trait dispatches XLaravel\Username\Events\UsernameChanged with the model and the old and new usernames — handy for redirecting old profile links, clearing caches or writing an audit entry:

use XLaravel\Username\Events\UsernameChanged;

Event::listen(function (UsernameChanged $event) {
    ProfileRedirect::create(['from' => $event->old, 'to' => $event->new]);
});

The event is dispatched only after the surrounding database transaction commits, so a rolled-back change is never announced. Giving a new model its first username does not count as a change.

The trait also adds two model events, so the change can be handled in an observer:

class UserObserver
{
    public function usernameChanging(User $user): bool
    {
        return ! $user->isLocked();
    }

    public function usernameChanged(User $user): void
    {
        $old = $user->getOriginal('username');
    }
}
  • usernameChanging runs before the row is written. Return false to cancel the save.
  • usernameChanged runs right after the row is written, inside the transaction if there is one. getOriginal() still returns the old username at that point.

Both can also be registered on the model, like any Eloquent event:

User::usernameChanged(fn (User $user) => Cache::forget("profile:{$user->getOriginal('username')}"));

Use the observer events for work that belongs with the save, and the UsernameChanged event for work that must only happen once the change is committed.

String aliases

$request->validate([
    'username' => ['required', 'username', 'not_reserved_username'],
]);

Both aliases use the config. not_reserved_username also takes extra names as parameters: not_reserved_username:acme,beta. Laravel resolves the aliases' messages like any extended rule, so add lines to your application's lang/xx/validation.php, or pass inline messages.

Customising the messages

The rule objects come with Turkish and English messages. To change them, publish the language files:

php artisan vendor:publish --tag=username-translations

They land in lang/vendor/username/{tr,en}/validation.php.

Testing

composer install
composer test
composer analyse

The suite runs on Orchestra Testbench against every supported Laravel and PHP combination in CI, along with PHPStan (Larastan) at its highest level.

License

Open source software licensed under the MIT license.