kadevland/simple-laravel-eloquent-uuid
A lightweight Laravel package that adds UUID primary keys to Eloquent models with minimal setup. Use a simple trait to generate UUIDs automatically, keep models clean, and avoid manual ID handling in your database.
Before you install, check compatibility. The last tagged release is from July 2020 (Laravel 6/7 era). Run a dry install first:
composer require kadevland/simple-laravel-eloquent-uuid --dry-run
If it resolves, install it. If it fights with ramsey/uuid, illuminate/*, or a PHP 8.1+ constraint, stop here and jump to the "Do you even need this?" note in Gotchas — Laravel ships HasUuids natively since 9.x.
Read the source first. This is a single-trait package (~20–40 lines). It will take you two minutes and tells you exactly what it does and does not set:
find vendor/kadevland -name "*.php" | xargs wc -l
cat vendor/kadevland/simple-laravel-eloquent-uuid/src/*.php
Confirm three things while you're in there:
creating (or creating + saving).$incrementing = false and $keyType = 'string' for you, or expects you to.if (empty($this->getKey()))).Minimal wire-up. In the model:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Kadevland\...\HasUuid; // use the FQCN you found above
class Invoice extends Model
{
use HasUuid;
// Set these yourself if the trait does not:
public $incrementing = false;
protected $keyType = 'string';
}
In the migration:
Schema::create('invoices', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('number');
$table->timestamps();
});
First use case to reach for: a resource whose ID is exposed to the outside world — invoice/receipt IDs, public share links, api/v1/orders/{id}, anything you'd otherwise leak in a URL. Do not reach for it on high-write internal tables like events, jobs, or sessions (see the index-fragmentation note below).
Sanity check:
$invoice = Invoice::create(['number' => 'INV-001']);
dump($invoice->id); // "9b1f...-...."; string, not int
Foreign keys and relations. Every referencing column must match the key type. unsignedBigInteger will silently truncate or error depending on your driver:
$table->foreignUuid('invoice_id')->constrained(); // Laravel 7+
$table->uuid('invoice_id'); // older, then ->foreign('invoice_id')->references('id')->on('invoices')
$table->uuidMorphs('taggable'); // polymorphic
Relations need no changes — belongsTo/hasMany resolve by $keyType:
public function invoice() { return $this->belongsTo(Invoice::class); }
Route model binding works out of the box only if $keyType = 'string' and $incrementing = false are set. If you forget, find() will cast the UUID to an int and return null — a classic five-minute bug.
Route::get('/invoices/{invoice}', fn (Invoice $invoice) => $invoice);
The "separate UUID column" pattern. Often better than swapping the PK. Keep the auto-increment id for joins and indexing, add a uuid column for the public surface:
$table->uuid('uuid')->unique();
// model: use HasUuid but point it at a non-key column, or just:
protected static function booted(): void
{
static::creating(fn ($m) => $m->uuid ??= (string) Str::uuid());
}
public function getRouteKeyName() { return 'uuid'; }
This sidesteps every migration and index issue in the Gotchas section and is what most teams end up doing.
Factories and tests. The UUID is assigned on the creating event, so make() gives you null:
Invoice::factory()->make()->id; // null
Invoice::factory()->create()->id; // uuid string
For deterministic tests, always pass an explicit ID — which also verifies the trait respects pre-set keys:
$invoice = Invoice::factory()->create(['id' => '00000000-0000-0000-0000-000000000001']);
Bulk import / restore / seeding. Because generation is event-driven, Model::insert(), DB::table()->insert(), and any unguarded raw query will not produce a UUID. Either loop over create(), or generate them yourself in the array:
foreach ($rows as $row) {
Invoice::create($row); // event fires, UUID assigned
}
// or
DB::table('invoices')->insert(
collect($rows)->map(fn ($r) => $r + ['id' => (string) Str::uuid()])->all()
);
If you want to migrate off later, Laravel 9+ is a drop-in swap:
use Illuminate\Database\Eloquent\Concerns\HasUuids; // replaces the package trait
Keep a base model (App\Models\BaseModel) that uses whichever trait, so the swap is one line instead of forty.
$incrementing and $keyType are the whole ballgame. Whatever the trait does internally, verify the model actually reports:
$m = new Invoice;
dump($m->getIncrementing(), $m->getKeyType()); // false, "string"
If getIncrementing() is true, Eloquent will try to read lastInsertId() after the insert and overwrite your UUID with 0 in the in-memory model. This is the single most common failure mode of these micro-traits.
Check for an existing-key guard. Many 2020-era traits unconditionally overwrite: $this->{$this->getKeyName()} = Str::uuid();. That destroys seeders, fixtures, and restore() flows. If it's unconditional, extend it in your base model:
protected static function bootHasUuid(): void
{
static::creating(function ($model) {
$model->{$model->getKeyName()} ??= (string) Str::uuid();
});
}
(Trait boot methods are boot{TraitName} — use that method name verbatim or you'll shadow nothing and everything, respectively.)
Column type is driver-specific. MySQL has no real uuid type — it's an alias for char(36). Postgres has a native uuid and will reject a varchar mismatch on FKs. SQLite doesn't care, so your tests pass and production fails. Pin it explicitly if you're multi-database.
InnoDB index fragmentation is real. Random UUIDv4 primary keys scatter inserts across the clustered B-tree. Fine for thousands of rows, painful at tens of millions. Mitigations:
Str::orderedUuid() (Laravel 7+) or UUIDv7 in your override instead of v4.binary(16) and cast, if you control both ends.uuid-column pattern above.Bypassed events. Anything that skips Eloquent events skips the UUID: insert(), upsert(), withoutEvents(), saveQuietly(), Model::unguard() bulk operations. Grep your codebase for ->insert( before you ship.
String IDs leak into typed APIs. Auth::id(), $request->user()->id, and Model::find() now return strings. Anything doing (int) $id, parseInt(id) in JS, or a PHP method signature function handle(int $userId) will break. Sweep your type hints.
Debugging. If no UUID appears, confirm the listener actually registered:
dump(Invoice::getEventDispatcher()->hasListeners('eloquent.creating: App\Models\Invoice'));
And log the insert:
DB::listen(fn ($q) => logger($q->sql, $q->bindings));
If the SQL contains "id" => 0 or omits id entirely, you've found an ordering problem — another observer on creating is likely firing first and short-circuiting, or the trait's boot method name doesn't match the trait's actual name.
Maintenance risk is the headline. 13 stars, no release since July 2020, and a package score of 0.095. It almost certainly has no Laravel 10/11 CI, and ramsey/uuid has moved major versions twice since. For a new project, the honest recommendation is Illuminate\Database\Eloquent\Concerns\HasUuids (or HasVersion7Uuids on 11+) — zero dependency, same behaviour, maintained. Pull this package in only for an existing codebase that already depends on it, and pin it in composer.json with a comment pointing at the native replacement.
No config, no migrations, no publishables. There's nothing to vendor:publish and nothing to configure — if a tutorial tells you otherwise, it's about a different package. Treat it as a pure code dependency and read the trait source as its documentation.
How can I help you explore Laravel packages today?