Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Username Laravel Package

x-laravel/username

A Laravel package for adding username support to Eloquent models, with configurable username generation, validation rules, uniqueness checks, and helper methods for registration and profile flows. Lightweight and easy to configure.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a complete username lifecycle layer for Eloquent models: format validation, reserved-name protection, generation from free text, uniqueness with history, and automatic pruning. Look at the published config file first — nearly every behaviour (format, reserved names, history retention, pruning cadence, connection, table/model names) is a config key.

Minimal setup:

  1. Install and publish assets:

    composer require <vendor>/<package>
    php artisan vendor:publish --tag=<package>-config
    php artisan vendor:publish --tag=<package>-migrations
    php artisan migrate
    
  2. Read config/<package>.php end to end before writing code. The format rule (character classes, min/max length, separators allowed) and the reserved-name list are both driven from here, so the config is effectively your product spec.

  3. Opt a model in — this is the only wiring step:

    class User extends Authenticatable implements HasUsername
    {
        use InteractsWithUsername;
    }
    

    The interface is how the model declares what its username is built from (return the name, or the email local-part, or a display name). The trait supplies the rest: generating a missing username, lowercasing a changed one, and retrying on a unique-index race.

  4. Confirm your users table has a unique index on the username column. The trait's race-condition retry only makes sense because the database enforces uniqueness; without the index you get silent duplicates instead of a retry.

First use case — registration without asking the user for a username:

$user = User::create([
    'name' => $request->name,
    'email' => $request->email,
    // username intentionally omitted — the trait generates it
]);

That is the shortest path to value. Everything else (history, events, pruning) is already on by default through config, so a fresh install immediately gives you "old usernames still resolve" and "old usernames are still blocked for their owner."

Implementation Patterns

Prefer the generator over ad-hoc slugging. UsernameGenerator::normalize() folds arbitrary text into a valid-format candidate; generate() additionally resolves collisions and checks reserved names. Use normalize() when you want a deterministic preview (e.g. showing the user what they'll get), and generate() when you need something that actually persists.

Validation in form requests. Three rules, three jobs — compose them rather than reimplementing:

'username' => [
    'required',
    'string',
    new Rules\Username,        // format only
    new Rules\NotReserved,     // reserved-name blocklist
    new Rules\UniqueUsername,  // unique *and* aware of reserved history
],

UniqueUsername is the one that matters on the update path: because history rows stay reserved for their owner for a configurable period, a username that looks free in the users table may still be rejected. Using unique:users,username here will produce a rule that disagrees with the package — always use UniqueUsername.

Self-service username changes. Let validation pass, assign the attribute, save; the trait lowercases the value, writes the old one into history, and dispatches events. The pattern that works well is: validate → assign → save() → react to UsernameChanged rather than doing side effects inline. Because the event is dispatched after commit, listeners can safely queue jobs (re-send marketing emails, update a search index, notify followers) without seeing an uncommitted row.

History as a feature, not a side effect. Route model binding, whereUsername() and findByUsername() all resolve old usernames automatically. That means old links keep working with zero extra code — an easy win to advertise to users. It also means you should not build your own "previous usernames" table; query the configured history model instead.

Model events vs. the event class. Use usernameChanging / usernameChanged for anything that must run in the same request and can veto or observe the change; use Events\UsernameChanged for cross-cutting, post-commit work. Mixing the two is the usual source of "notification fired but the change rolled back."

Multi-tenant / multi-database setups. The configurable connection, history table and history model exist for this. Point the connection at the tenant DB and keep the same config shape; do not override the table per-tenant unless you also override it consistently in the history model.

Scheduling and pruning. Pruning rides model:prune and the package registers its own schedule with configurable frequency, age and chunk size. In practice: keep the package's schedule entry (don't disable schedule:run), and tune chunk size down if history rows are wide or the table is hot. In tests, disable pruning or set a long age so fixtures don't evaporate mid-suite.

Customising the format per form. Every part of the Rules\Username rule is configurable on the rule instance as well as in config. Good pattern: keep the config as the default, and pass explicit parameters on the rule only where a specific flow genuinely differs (e.g. an imported legacy account), so the invariant "anything generated passes the rule" holds.

Gotchas and Tips

  • The generator and the rule must agree. If you tighten Rules\Username via config after users exist, generate() should still produce conforming values, but previously stored usernames may no longer pass. Decide whether you validate existing rows before deploying a config change.
  • Reserved names are a moving target. NotReserved is backed by a config list. Adding a name does not retroactively free or seize existing accounts; it only affects new assignments and changes. Removing a name doesn't release history reservations either.
  • History reservations expire. The "reserved for their owner" period is configurable; after it lapses, someone else can take the username and the old URL stops resolving to the original owner. Set this deliberately — it is a product decision disguised as a config value.
  • Lowercasing is unconditional. Any changed username is lowercased by the trait. Don't build case-preserving display names into the username column; store the pretty version separately.
  • The unique-index retry is not a lock. The trait retries a generated username that loses a race. That covers concurrent generation, not two users deliberately submitting the same desired username — those should still fail validation with a normal error, which is what UniqueUsername is for.
  • Route binding ordering. If you have both a users route key and a custom binding, confirm old usernames resolve as expected — binding goes through the history path, so a stale cached route or a manually written User::where('username', ...) in a controller bypasses it.
  • after commit semantics in tests. UsernameChanged fires after commit; inside DatabaseTransactions tests the commit never happens the way you expect. Use RefreshDatabase (or Event::fake()) rather than asserting on listeners inside a rolled-back transaction.
  • Config caching. Reserved names, format knobs and pruning cadence are read from config. Run php artisan config:cache in deploy, and remember the schedule is registered at boot — changing prune frequency requires a deploy, not just a config tweak.
  • Translations. The username and not_reserved_username aliases ship with English and Turkish messages. Publish/override the lang files if you support other locales, rather than writing message strings in form requests.
  • Extension points worth knowing: the history model (subclass it for extra columns, keep the contract), the history table/connection keys, and the rule constructor arguments. Those three cover almost every customisation request; reaching past them into the trait usually means fighting the package.
  • Debugging duplicates or "username suddenly changed": check in this order — unique index present? trait applied? UsernameChanged listener doing an assignment and re-saving? history pruning age set to zero/very short in a local .env?
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo