alsofronie/eloquent-uuid
alsofronie/eloquent-uuid adds UUID primary key support to Laravel Eloquent models, with traits that automatically generate UUIDs on create and let models replace auto-incrementing integer keys with minimal configuration.
Install and pin the version. This package targets Laravel 5.1/5.2 and its last release was July 2017, so treat it as a locked-in dependency of an existing 5.x app rather than something to introduce into a new project:
composer require alsofronie/eloquent-uuid
Add the column. The trait generates a UUID string before insert, so the migration just needs somewhere to put it. Keep the auto-increment id as the primary key and add the UUID as a unique secondary key — this is the lowest-friction option and avoids rewriting every foreign key:
Schema::create('posts', function (Blueprint $table) {
$table->increments('id');
$table->uuid('uuid')->unique(); // char(36) on MySQL
$table->string('title');
$table->timestamps();
});
Attach the trait. This is the whole integration — the trait's boot...() hook registers a creating listener that fills the column:
use Alsofronie\Uuid\UuidModelTrait;
class Post extends Model
{
use UuidModelTrait;
// $uuidColumn defaults to 'uuid'; override if you named it differently
// protected $uuidColumn = 'guid';
}
First use case: getting opaque identifiers out of your URLs. The immediate payoff is that /posts/1 becomes /posts/9f1c.... Wire it into route model binding by overriding the route key, then controllers and views need no changes at all:
public function getRouteKeyName()
{
return 'uuid';
}
Before building anything else, verify the trait is actually booting: create a record in php artisan tinker and inspect $post->uuid. Because the value is assigned in creating, it is null on a fresh new Post — that surprises people on their first read-through.
Two trait flavours. The package ships a dashed (36-char) trait and a compact 32-char variant (Uuid32ModelTrait) with no hyphens. Pick one per project and stay consistent — the column length, the route regex, and any external API contract all depend on it. Mixing them across tables is a reliable source of "why doesn't findByUuid work here" bugs.
Lookup helpers instead of where('uuid', ...). Use the trait's findByUuid($uuid) / findByUuidOrFail($uuid) in controllers and jobs. They centralise the column name and return null vs. throwing ModelNotFoundException, which maps naturally onto a 404:
Route::get('/posts/{uuid}', function ($uuid) {
return Post::findByUuidOrFail($uuid);
});
Keep UUIDs out of mass assignment. Since the value is generated server-side, do not add uuid to $fillable. If you must accept a client-supplied UUID (imports, fixtures), handle it explicitly rather than through create($request->all()).
Relationships: reference the integer key, expose the UUID. The pragmatic pattern in a 5.x codebase is to keep user_id foreign keys pointing at the auto-increment id and only surface uuid in URLs and API payloads. Switching foreign keys to UUIDs means char(36) joins on every query — measurable on MySQL, and it forces a rewrite of every existing relation. If you do go UUID-everywhere, make it a deliberate migration project, not a side effect of adding a trait.
API resources / transformers. In toArray() or a fractal transformer, swap id for uuid at the boundary:
public function toArray($request)
{
return ['id' => $this->uuid, 'title' => $this->title];
}
Testing. With RefreshDatabase and factories, UUIDs are random per run, so assert on presence and format (Str::isUuid) rather than on exact values — or make the column accept an explicit value in the factory state so you can pin it for HTTP assertions:
$factory->defineAs(Post::class, 'fixed', function () {
return ['uuid' => '00000000-0000-4000-8000-000000000000'];
});
Note the trait may still overwrite it on creating; if so, assert by capturing $post->uuid after creation and building the URL from that.
Backfilling an existing table. Add the column nullable, deploy, then run a one-off migration or command that iterates rows and assigns (string) Str::uuid(), then add the unique index and flip to NOT NULL. Doing all three in one migration will fail on any table with rows.
The package is effectively unmaintained. Last release 2017-07-20, targeting Laravel 5.1/5.2. It will not work with Laravel 6+ without patching, and it is not a candidate for a dependency upgrade path. On Laravel 9+ the framework ships Illuminate\Database\Eloquent\Concerns\HasUuids (and HasUlids), which covers this use case with first-party support — migrating to that is strictly better than pinning a 2017 package. On 5.5–8 the idiomatic replacement is a five-line hand-rolled trait:
protected static function bootHasUuid()
{
static::creating(function ($model) {
$model->{$model->uuidColumn ?? 'uuid'} = (string) Str::uuid();
});
}
Given the low opportunity score, budget for this replacement rather than building architecture around the package's API.
Column type mismatch. $table->uuid() becomes char(36) on MySQL and uuid on Postgres. Do not use binary(16) optimisations with this package — the trait writes a string, so a binary column will silently corrupt or truncate. Match the column length to the trait variant (36 vs 32).
uuid is null until save. Any code that reads $model->uuid before save() — event listeners on saving ordering, form repopulation, queue payloads built from unsaved models — will see null. If you need the identifier earlier (e.g. to build a filename), assign it manually first; the trait typically respects an already-set value, but confirm that in your version rather than assuming.
Uniqueness is enforced by the database, not the trait. There is no retry loop on collision (astronomically unlikely for v4, but a real concern if you subclass and use v1/time-based generation). Keep the unique index; do not rely on application-level checks.
Route binding with soft deletes and scoping. getRouteKeyName() returns a single column globally — it does not compose with Route::bind closures or implicit scoping. If you have tenanted models, scope in a Route::bind or a resolveRouteBinding override instead, and remember soft-deleted records will 404 through implicit binding.
Override points. The trait is small; the useful hooks are the column-name property and the UUID-generation method. Subclass or copy the trait if you need v5 (namespace-based, deterministic) UUIDs — deterministic v5 is genuinely useful for idempotent imports, and swapping the generator is the cleanest way to get there.
Debugging checklist when a UUID isn't appearing: (1) trait actually imported inside the class body, not just used at the file top; (2) column exists and is in the migration that actually ran; (3) $uuidColumn matches the real column name; (4) you're not calling insert()/DB::table() — query-builder writes bypass Eloquent events entirely, which is the single most common cause of missing UUIDs in bulk imports. For bulk work, chunk through Eloquent create() or generate the UUIDs yourself in the array.
How can I help you explore Laravel packages today?