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

Laravel Eloquent Uuid Laravel Package

bmatovu/laravel-eloquent-uuid

A Laravel package that adds UUID support to Eloquent models, letting you use universally unique identifiers as primary keys instead of auto-incrementing integers. It handles key generation and configuration for UUID-based models.

View on GitHub
Deep Wiki
Context7

Getting Started

Read this first: this package is archived, has 3 stars, zero dependents, and its last release was January 2022 — weeks before Laravel 9 shipped the built-in Illuminate\Database\Eloquent\Concerns\HasUuids. Treat it as a stopgap for Laravel 6–8 projects, not a foundation for new work. If you're on Laravel 9+, skip to the "Gotchas" section and use the native trait instead; the concepts below transfer directly.

Install (only on Laravel ≤ 8):

composer require bmatovu/laravel-eloquent-uuid

There is no service provider to register if the package is trait-only — check composer.json's extra.laravel.providers and the README before adding anything to config/app.php. The whole surface area is small enough to read in one sitting: open the src/ directory and find the trait (something like HasUuid) plus any supporting generator class. Read those two files before writing any code — you need to know when the UUID is assigned (almost certainly a creating model event) and whether the trait also flips $incrementing/$keyType for you.

First use case — a posts table keyed by UUID:

  1. Migration, using Laravel's own blueprint helper (you don't need the package for this):
Schema::create('posts', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->string('title');
    $table->timestamps();
});
  1. Model, applying the package trait:
use Bmatovu\...\HasUuid; // confirm exact FQCN from src/

class Post extends Model
{
    use HasUuid;

    protected $guarded = [];
}
  1. Verify the trait did its job before trusting it:
$post = Post::create(['title' => 'Hello']);
dump($post->id, $post->incrementing, $post->getKeyType(), $post->getKeyName());
// expect: a 36-char string, false, 'string', 'id'

If $incrementing is still true, the trait is incomplete and you must set it manually — this is the single most common failure mode with small UUID packages. Add it in the model or a base class:

protected $incrementing = false;
protected $keyType = 'string';

Implementation Patterns

App-wide base model. Rather than sprinkling the trait, centralize the three settings that UUID keys demand:

abstract class UuidModel extends Model
{
    use HasUuid;

    protected $incrementing = false;
    protected $keyType = 'string';
}

Everything that extends UuidModel inherits consistent behavior, and you have one place to swap in ordered UUIDs later.

Hybrid: auto-increment PK + public UUID column. Often the pragmatic choice — keeps integer joins fast and small, exposes an unguessable identifier in URLs. Use uniqueIds() on Laravel 9+ (HasUniqueStringIds in 11+), or on this package just set the column yourself in a booted() hook:

protected static function booted(): void
{
    static::creating(fn ($model) => $model->uuid ??= (string) Str::orderedUuid());
}

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

Route model binding works unchanged with a string key, but be aware the resolver passes whatever is in the URL straight to where('id', $value). Guard against non-UUID input in hot routes (a ->whereUuid() cast or a Route::pattern('post', '[0-9a-f-]{36}')) so malformed IDs fail fast instead of hitting a full-table scan on some drivers.

Relationships must match types. The foreign key column has to be the same type as the parent key, or MySQL will silently compare/cast and drop your index:

$table->foreignUuid('post_id')->constrained()->cascadeOnDelete();

Model side is ordinary: hasMany(Comment::class). The gotcha is remembering it for every child migration — mismatched unsignedBigInteger vs char(36) is the #1 bug when retrofitting UUIDs onto an existing schema.

Factories and seeders. Factories should not set id — let the trait assign it. Keep the factory's definition() free of primary key entries, and if you need deterministic IDs in tests, override explicitly: Post::factory()->create(['id' => $known]).

Testing. RefreshDatabase and assertDatabaseHas work fine, but assertDatabaseHas('posts', ['id' => $post->id]) requires the ID to actually be a string in the DB — assert against (string) $post->id, not an int cast.

Bulk imports. Inserting thousands of rows through Eloquent with a creating hook is slow (one UUID per model, one model event per row). For imports, generate IDs up-front and use insert() directly — but remember events don't fire, so you own ID generation entirely:

$rows = collect($data)->map(fn ($r) => [
    'id' => (string) Str::uuid(),
    'title' => $r['title'],
    'created_at' => now(),
    'updated_at' => now(),
]);
Post::insert($rows->all());

Gotchas and Tips

The $incrementing/$keyType trap. This is the failure mode that wastes an afternoon: the trait generates a UUID, the model still thinks it has an auto-increment key, and MySQL coerces '9f8b...' to 0. First insert "succeeds", second throws a duplicate-key error on id = 0. Always assert on a fresh model that getKeyType() === 'string' and getIncrementing() === false. If the package's trait doesn't set them, do it in a base model — don't rely on remembering per model.

Random UUIDs fragment your indexes. A v4 UUID primary key on InnoDB means random B-tree inserts, larger pages, and slower range scans as the table grows. If you keep UUID PKs, switch the generator to Str::orderedUuid() (time-ordered, still non-guessable) and override the package's generator or the model's creating hook. For high-write tables, prefer the hybrid pattern — integer internal PK, UUID as the public identifier.

Storage size. $table->uuid() is char(36) — 36 bytes per key, plus it's carried in every secondary index. At scale, consider binary(16) with conversion casts, or accept the cost and document it. Don't mix storage formats across tables; it will bite you at join time.

Events are the mechanism, and events are bypassable. Because assignment almost certainly happens in creating, anything that skips Eloquent events (Model::insert(), DB::table()->insert(), some queue/bulk packages) will produce null or 0 IDs. If you see Duplicate entry '0' or Column 'id' cannot be null in bulk paths, this is why.

Don't let id be mass-assignable. With UUIDs, an attacker-supplied id is more plausible to slip through review than with auto-increment. Use $guarded = ['id'] or an explicit $fillable that excludes it — and if you do want caller-supplied IDs (client-generated IDs for offline sync), validate them as UUIDs and handle the unique-constraint violation as a conflict, not a 500.

Migration order and rollbacks. UUID PKs are fine with RefreshDatabase, but migrate:rollback on a schema with foreignUuid constraints needs the children dropped first — standard Laravel behavior, just more visible here because you're likely retrofitting onto a live DB. For existing tables, add the UUID column nullable → backfill → then add the unique index and the NOT NULL constraint; don't do it in one ALTER.

Maintenance risk is the real gotcha. Archived since early 2022, no dependents, 3 stars. It won't track Laravel's changes to Model, serialization, or HasUniqueStringIds. Plan the exit: on Laravel 9+, replace the trait with Illuminate\Database\Eloquent\Concerns\HasUuids; on 11+, HasUniqueStringIds with a uniqueIds() method. The migration is usually a one-line use swap plus deleting the manual $incrementing/$keyType declarations — which is exactly why you put them in a base class in the first place.

Debugging checklist when IDs look wrong: (1) dd($model->getKeyType(), $model->getIncrementing()); (2) confirm the creating listener actually runs (static::creating(fn () => dump('fired'))); (3) check the generated value's format — a v4 UUID is version-4 (Str::uuid()), ordered UUIDs differ, and a truncated value means the DB column is too short; (4) inspect the raw insert with DB::listen(); (5) confirm you're not in a withoutEvents() block (tests, imports, some observers).

Extension points are thin, so use the model, not the package: override the generation method (or the creating hook), swap Str::uuid() for Str::orderedUuid(), and add a HasUuids-style interface in your app so the swap to native Laravel later is mechanical. Don't fork the package for this — three lines in a base model achieves the same thing with zero dependency risk.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager