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

Uid Keys Laravel Package

brokeyourbike/uid-keys

Laravel package that swaps auto-incrementing integer primary keys for unique UID keys on your Eloquent models. Add the trait and new records get collision-resistant string IDs automatically—ideal for APIs, distributed systems, and hiding sequential IDs.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a thin convenience layer: it gives an Eloquent model string primary keys generated as UUIDs or ULIDs, without you hand-rolling boot() hooks on every model. It is not an ORM replacement, not a migration generator, and not a UUID/ULID library — it wraps Str::uuid() / Str::ulid() (or ramsey/uuid / Symfony's Ulid) behind a trait and a config file.

Minimal steps:

composer require brokeyourbike/uid-keys
php artisan vendor:publish --tag="uid-keys-config"   # verify the tag in the README

The service provider is auto-discovered. Then opt a model in:

use Brokeyourbike\UidKeys\HasUidKeys; // confirm exact FQCN against the README first

class Post extends Model
{
    use HasUidKeys;
}

and declare a matching column type in the migration:

Schema::create('posts', function (Blueprint $table) {
    $table->ulid('id')->primary();   // or $table->uuid('id')->primary()
    $table->string('title');
    $table->timestamps();
});

First use case worth trying: create a table you would otherwise expose as auto-increment, migrate it, then run Post::create(['title' => 'x'])->id in tinker. You should get a 26-character ULID (or 36-character UUID) with no extra code. That single loop — trait + migration column + tinker — tells you whether the package's defaults match your schema conventions before you roll it across models.

Where to look first: the config file (UUID vs ULID default, and any column-type mapping), then the trait source. The whole package is small enough to read in five minutes, and you should — with zero stars and zero dependents, the source is the documentation.

Implementation Patterns

Per-model opt-in, not global. The idiomatic use is to apply the trait only to models whose IDs are public-facing (users, posts, orders) and leave internal tables on bigIncrements. Blanket-applying string keys to every table costs you index size and join speed for no benefit on tables nothing external references.

Pick the type deliberately. UUIDv4 is opaque and non-sortable — good for IDs you never order by. ULID is lexicographically time-ordered — good for feeds, cursor pagination, and tables where you would otherwise orderBy('created_at'). If the config lets you set the default, set it once to match your dominant use case and override on the exceptions.

Relationships must agree on storage. The single most common integration bug:

// parent
$table->ulid('id')->primary();

// child — must be exactly the same type and length
$table->foreignUlid('post_id')->constrained();
// NOT $table->foreignUuid(...), NOT $table->string('post_id', 36)

Same for pivot tables: foreignUlid('user_id') and foreignUlid('role_id'), both ->index().

Factories. Don't set the ID in the factory definition — the trait generates it on creating. When you do need determinism for assertions:

$post = Post::factory()->create(['id' => (string) Str::ulid()]);

Confirm the trait only fills the key when it's empty; if it unconditionally overwrites, explicit-ID imports and fixtures break, and you'll need forceFill() after save or a separate non-trait path.

Route model binding needs no changes. Route::get('/posts/{post}') resolves by the string key as-is because $keyType = 'string' and $incrementing = false. Just don't reintroduce getRouteKeyName() returning something else by copy-paste from an old model.

Validation and API shape. IDs are JSON strings. Use Laravel's ulid / uuid validation rules rather than integer. Anything consuming your API that assumes a numeric id — mobile clients, generated SDKs — is a breaking change; plan for it.

Ordering by ULID. orderBy('id') is a cheap stand-in for orderBy('created_at') on single-writer tables. It is not a strict guarantee across multiple app servers creating rows in the same millisecond, so keep created_at as the tiebreaker if ordering is load-bearing:

->orderBy('id')->orderBy('created_at')

Cursor pagination. cursorPaginate() works well with ULIDs precisely because they're monotonic; this is the pattern where the ULID choice pays for itself.

Backfilling an existing table. The package does not migrate anything. The safe path is additive: add a nullable ulid column, backfill in chunks, add a unique index, switch reads, then drop the old primary key in a later deploy. Doing it in one migration on a large table will lock it.

Gotchas and Tips

  • Type/length mismatch is silent until it isn't. A ULID (26 chars) written into a char(36) UUID column "works" until something compares or validates it as a UUID. In MySQL strict mode a UUID written into char(26) errors; in non-strict mode it truncates and you get duplicate-key errors that look like generator bugs. Check SHOW CREATE TABLE before debugging anything else.
  • The trait's boot hook. If your model defines its own boot() or bootHasUidKeys(), you can silently shadow the package's creating listener and get null IDs. Symptom: save() succeeds, id is null or 0 after refresh. Fix: call parent::boot() and rename your hook.
  • $incrementing / $keyType conflicts. The trait sets both. Any model property or another trait overriding them produces SQLSTATE[23000] on insert or a cast to int on retrieval. Grep your traits before adding this one to a heavily-traited model.
  • No integer casts anywhere. (int) $model->id becomes 0 and 1 silently. Audit exports, CSV writers, third-party packages with numeric-key assumptions (some admin panels, audit log packages, legacy queue payloads).
  • Config cache. Changing the default key type in config does nothing after php artisan config:cache until you re-cache. This catches people in CI where the cache is warmed once.
  • Mass assignment. id is not fillable, so Model::create(['id' => ...]) drops it. For seeders and imports use forceFill() or add id to $fillable on the specific model that needs explicit keys.
  • Binary storage. If the config offers binary(16) storage, migrations must use the matching column and models need a cast. Mixing binary storage with a char(36) column is a guaranteed silent corruption. Stick to the char-based default unless you've measured the index size problem.
  • Index bloat is real. A char(36) primary key plus three foreign keys is roughly 4–6× the index footprint of bigint. On high-row-count tables, benchmark before committing; on small reference tables, don't care.
  • Zero adoption is the main risk. With no dependents and a single maintainer, treat the trait as vendorable: it's small enough that if the package stalls, copying it into app/Concerns/ is a 20-minute job. Pin the version in composer.json and avoid depending on undocumented config keys.
  • Extension point. If the package exposes a generator method (something like generateUid()), override it in your model to control the source — useful for injecting a deterministic generator in tests or a UUIDv7 implementation. If it doesn't, wrap it: bind your own generator and have the trait call through a service, so swapping later isn't a rewrite.
  • Testing. RefreshDatabase plus assertDatabaseHas('posts', ['id' => $post->id]) is all you need; there's no special test helper. Assert the format once (Str::isUlid($post->id)) in a model test so a config drift or a dependency bump that changes the generator fails loudly rather than at the database.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core