ryangjchandler/laravel-uuid
A lightweight Laravel package that brings UUID support to Eloquent models. It provides a simple trait for generating UUID primary keys and resolving models by UUID, making it easy to use UUIDs in place of auto-incrementing IDs.
Install and publish
composer require ryangjchandler/laravel-uuid
php artisan vendor:publish --tag="laravel-uuid-config" # if you need to customize the column/version
The package is tiny — there is no service provider surface to learn beyond the config file and one trait. Read the trait source first (usually src/HasUuids.php); it is short enough to audit in a couple of minutes and tells you exactly which model event the UUID is assigned on.
Wire it into a model
use RyanChandler\LaravelUuid\HasUuids;
class Post extends Model
{
use HasUuids;
}
Migration — match the column type the package writes to:
Schema::create('posts', function (Blueprint $table) {
$table->id(); // keep the auto-increment PK if using a secondary column
$table->uuid('uuid')->unique();
// or, if the UUID *is* the key:
// $table->uuid('id')->primary();
$table->timestamps();
});
First use case: you have an internal id you don't want to leak in URLs, API payloads, or exports. Add the trait, add a uuid column, point route model binding at it, and you're done:
public function getRouteKeyName(): string
{
return 'uuid';
}
That is the 80% case — a public-facing opaque identifier alongside a fast integer primary key.
Secondary column (recommended default). Keep the integer PK for joins and index density, and use the UUID purely as an external identifier. Hide the integer in API resources:
class PostResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->uuid,
'title' => $this->title,
];
}
}
UUID as the primary key. Only do this if you accept the trade-offs (larger indexes, no natural ordering). You typically need to tell Eloquent about it:
protected $keyType = 'string';
public $incrementing = false;
Check whether the trait already sets these — some versions do, some expect you to. Verify with php artisan tinker after creating a record.
Route model binding by UUID. Set getRouteKeyName() to the UUID column and make sure the column is indexed/unique. Controllers and Route::resource calls then need no changes:
Route::get('/posts/{post}', [PostController::class, 'show']);
// resolves via uuid, 404s correctly on unknown values
Combining with relationships. Foreign keys must use the same column type as the referenced key. If you reference a UUID primary key:
$table->foreignUuid('post_id')->constrained();
If you use a secondary UUID column, reference that column explicitly: ->constrained('posts', 'uuid').
Factories and seeders. Factories generally don't need to set the UUID — the trait fills it on creating. But seeding with Model::insert() or upsert() bypasses model events entirely, so generate UUIDs manually in bulk paths:
Post::insert($rows->map(fn ($r) => [...$r, 'uuid' => (string) Str::uuid()])->all());
Testing. Assert the shape rather than exact values:
$this->assertTrue(Str::isUuid($post->uuid));
$this->assertSame($post->uuid, Post::find($post->id)->uuid); // persisted, not just in-memory
Illuminate\Database\Eloquent\Concerns\HasUuids and HasUlids exist natively. If you're on a modern Laravel, prefer the framework trait unless this package gives you a specific feature (ordered UUIDs, a separate column, a particular UUID version). Adopting a third-party trait that shadows a framework concern creates confusion for future maintainers.insert, upsert, updateOrCreate on the query builder, and raw SQL will not trigger the creating event. Always backfill UUIDs in those code paths, or you get NULL violations / duplicate empty strings.$table->uuid() is char(36) on MySQL but a native uuid type on Postgres. Mixing a char(36) foreign key with a native uuid column will fail on Postgres — keep both sides consistent.php artisan config:clear (or re-cache in deploy). A stale cached config is the usual cause of "it's writing to the wrong column."creating. If you need deterministic IDs (fixtures, imports), check the trait's guard clause or set the attribute after create() + save().{post} returns 404 for a valid UUID, your route key name and the actual column disagree, or you never added the unique() index and an older row is NULL.used on the model, confirm the model fires creating (log it), confirm the column exists in the DB, then confirm the value persists after refresh(). That ordering finds the problem in under a minute.How can I help you explore Laravel packages today?