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

Eloquent Uuid Laravel Package

eig/eloquent-uuid

A Laravel package that lets Eloquent models use UUID primary keys instead of auto-incrementing integers. Drop it into existing models to generate unique, non-sequential IDs automatically for safer, distributed-friendly records.

View on GitHub
Deep Wiki
Context7

Getting Started

Check the version ceiling before you install. Release 3.0.0 (2022-12-08) requires PHP 8.0+, pins illuminate/* to Laravel 9.x, and adds a runtime dependency on eig/UUID 2.5.x. On a Laravel 10/11 app the resolver will refuse, or resolve something nobody tested against your framework. Confirm your composer.json Laravel constraint first — if you're not on 9.x, the day-to-day answer is Laravel's native HasUuids.

composer require eig/eloquent-uuid

Read the whole package before you trust it — it's ~40 lines. Everything lives in one trait under vendor/eig/eloquent-uuid/. Confirm three things, because that's where the magic and the breakage both are:

  1. The boot…() method that hooks Eloquent's creating event and assigns the key.
  2. Whether it sets $incrementing = false / $keyType = 'string' as trait properties or via an initialize…() method.
  3. Whether the assignment is guarded (if (! $model->getKey())) or unconditional — this decides whether client-supplied IDs survive.

Copy the trait FQN from the vendor directory, not from a blog post; the namespace and hook names changed between 2.x and 3.0.0.

First use case: greenfield tables where opaque IDs earn their keep — orders, invoices, api_tokens, webhooks, share links. Not a retrofit.

// migration
Schema::create('orders', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->foreignUuid('user_id')->constrained();
    $table->string('status');
    $table->timestamps();
});
// app/Models/Order.php
use Eig\UUID\EloquentUUID\HasUuid; // verify namespace against vendor/

class Order extends Model
{
    use HasUuid;
    protected $guarded = [];
}

Verify in tinker before writing another line:

$o = Order::create(['user_id' => 1, 'status' => 'pending']);
$o->id;                 // "f47ac10b-58cc-4372-a567-0e02b2c3d479"
$o->getKeyType();       // 'string'
$o->getIncrementing();  // false

If getIncrementing() is still true, the trait didn't boot — almost always because it was applied on a parent class or shadowed by a child model, rather than used inside the model body.

Implementation Patterns

Full UUID primary keys in a new bounded context

This is the only case where the package genuinely pulls its weight for a day-to-day developer: a new service or v2 API where every model in that context takes a UUID PK and every relation uses foreignUuid. Route-model binding keeps working untouched because the PK is id, and getRouteKeyName() already defaults to it.

$order = Order::create(['user_id' => $user->id, 'status' => 'pending']);

// relations are completely unchanged
public function user(): BelongsTo { return $this->belongsTo(User::class); }

Integer PK + public uuid column (the realistic retrofit)

You cannot put UUID primary keys on populated tables without dropping and rebuilding every FK. Keep the integer id internally and add a public identifier — which means you don't need this package at all, just the generator it depends on:

Schema::create('invoices', function (Blueprint $table) {
    $table->id();
    $table->uuid('uuid')->unique();
    // ...
});

// Invoice.php
protected static function booted(): void
{
    static::creating(fn (Invoice $i) => $i->uuid ??= (string) \Eig\UUID\Uuid::generate());
}

public function getRouteKeyName(): string { return 'uuid'; }

If this is your situation, be honest about it in the PR description: you're using eig/UUID plus a three-line hook, not the trait.

Mixed adoption — apply it deliberately

Don't grep-and-apply HasUuid across the codebase. A defensible split:

  • Trait on the handful of genuinely public-facing models (users, orders, invoices, share links).
  • Integer PKs on join tables, audit logs, cache, jobs, and anything sharded by write throughput.

Write the rule down — a one-paragraph ADR or a comment in AppServiceProvider — so the next contributor doesn't add it to every new model out of habit.

Binary (16-byte) storage

The compact-storage option is a schema and cast decision, not a trait feature. The trait just writes a string onto the attribute; you handle conversion.

Schema::create('orders', function (Blueprint $table) {
    $table->binary('id', 16)->primary();
});

// Order.php
protected $casts = ['id' => 'string']; // or a custom cast that packs/unpacks
protected $keyType = 'string';
public $incrementing = false;

Check the trait's guard before relying on this — if it overwrites unconditionally, a pre-packed value gets clobbered.

Client-generated IDs / offline-first ingestion

If a mobile client mints the UUID and pushes it up, the trait must respect it. Read the boot…() method: with if (! $model->getKey()) you're fine; with an unconditional overwrite, override booted() in the model or bypass the trait on that ingestion path entirely.

Serialization

Because $keyType = 'string', toArray() and JsonResource emit strings with no extra casts. The failure mode is on the consumer side:

fetch(`/orders/${order.id}`)   // good
parseInt(order.id)             // NaN

Testing

it('assigns a UUID primary key on create', function () {
    $order = Order::factory()->create();
    expect($order->id)->toBeString()->toMatch('/^[0-9a-f-]{36}$/');
    expect($order->getIncrementing())->toBeFalse();
    expect($order->getKeyType())->toBe('string');
});

Assert getIncrementing() and getKeyType() explicitly. A framework or eig/UUID bump can silently break those while UUIDs still look fine, and you want the test to fail on the semantics, not just the string format.

Gotchas and Tips

Bulk writes bypass the trait completely. Model::insert(), upsert(), DB::table(...)->insert(), query-builder seeders, and raw SQL never fire the creating event — you get NULL keys and broken FKs downstream. Add a DB-level default (UUID() on MySQL 8) or a Str::uuid() fallback inside import/seed paths. This is the single most common way UUID columns break in production.

Never combine with HasUuids. Laravel 9 ships HasUuids and HasUlids natively. Mixing them on one model gives you two creating hooks racing on id and two competing $keyType/$incrementing assignments. Pick one.

Custom primary keys aren't guaranteed. Traits of this shape typically only handle a key named id and only when nothing is set. Non-id PK, or a UUID on a non-PK column? Re-read the trait, fork it, or replace it with your own booted() hook.

char(36) vs binary(16) is where you pay. $table->uuid() is char(36) on MySQL — 36 bytes in every secondary index and every foreign key. Small tables won't notice; write-heavy tables will. Decide before schema freeze, because changing it later is a migration.

v4 UUIDs are non-monotonic. Random v4 on InnoDB causes B-tree page splits and index bloat on insert-heavy tables, and the package does nothing about it. If you need ordering locality, you want ULIDs or UUIDv7 — not this.

You now track two packages from one maintainer. 3.0.0 adds eig/UUID 2.5.x as a runtime dependency. Pin exactly ("eig/eloquent-uuid": "3.0.*") and add both to your dependency inventory, or vendor both. 59 stars, 0 dependents, and a December 2022 last release means you are the testing.

Verify behaviour against 3.0.0, not 2.x. The 2.x → 3.0 jump moved the PHP/Laravel floors and pulled in eig/UUID. Any StackOverflow answer older than December 2022 describes a different trait name and possibly different key-type handling.

Debugging checklist when something looks wrong:

$m = Order::first();
$m->getKeyName();       // 'id'
$m->getKeyType();       // 'string'
$m->getIncrementing();  // false
$m->toArray()['id'];    // UUID string, not null

incrementing === true means the trait didn't boot. id === null on rows that came from a seeder or import means a bulk path skipped creating.

The best extension point is to fork it. At ~40 lines, overriding the generator in a subclass booted() hook is entirely reasonable — and it drops both the eig/UUID dependency and the Laravel 9 ceiling in one move. If you need ordered IDs, a binary cast, or a different column name, forking is the correct answer, not patching around the trait.

Treat the Laravel 9 ceiling as a scheduled task, not a surprise. Laravel 9 left security support in February 2024. If you adopt 3.0.0, add "migrate to native HasUuids or promote the fork" to the roadmap on the same day you install it — not six months later when a framework upgrade forces the issue.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package