xzxzyzyz/laravel-uuid-model
Add UUID primary key support to Laravel Eloquent models. This package helps replace auto-incrementing IDs with universally unique identifiers, useful for distributed systems, public-facing IDs, and safer record references, while keeping model integration simple.
Install the package:
composer require xzxzyzyz/laravel-uuid-model
No service provider registration is needed on Laravel 5.5+ (auto-discovery). On older versions, add Xzxzyzyz\LaravelUuidModel\UuidModelServiceProvider to config/app.php.
Publish any config (only if the package ships one):
php artisan vendor:publish --provider="Xzxzyzyz\LaravelUuidModel\UuidModelServiceProvider"
Create the migration with a UUID primary key — do not use $table->id():
Schema::create('posts', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('title');
$table->timestamps();
});
Apply the package's trait to your model (typically HasUuid):
use Illuminate\Database\Eloquent\Model;
use Xzxzyzyz\LaravelUuidModel\Traits\HasUuid; // verify exact namespace in vendor/
class Post extends Model
{
use HasUuid;
}
Your first use case — "just create it and the key is populated":
$post = Post::create(['title' => 'Hello']);
// $post->id is now a UUID string, no manual assignment needed
Where to look first: open vendor/xzxzyzyz/laravel-uuid-model/src/ and read the trait — it is almost certainly a ~30-line boot hook overriding getIncrementing(), getKeyType(), and bootHasUuid(). There is no magic beyond that; knowing exactly what it does saves you hours of guessing later. Cross-check the README's migration and model examples before writing your own.
Trait on each model. The idiomatic usage is one use HasUuid; per model that needs a UUID PK. This keeps models explicit and greppable.
Base model wrapper. For apps where every model is UUID-keyed, create an abstract UuidModel extends Model that applies the trait once, and have your models extend it:
abstract class UuidModel extends Model
{
use HasUuid;
}
This centralizes the behavior and makes onboarding new models a one-line decision (which parent to extend). Trade-off: it hides the trait from a quick grep.
Custom column name. If the package exposes a configurable column (usually via a property like $uuidColumn or an overridable getUuidColumn()), override it in the model when your id is not the primary key or when you have a secondary UUID (e.g., a public uuid column alongside an auto-increment id). Verify the hook exists before relying on it — if it does not, generate it yourself in a creating observer.
Foreign keys. In related migrations, use foreignUuid('post_id')->constrained() (Laravel 8+) or $table->uuid('post_id') plus a manual foreign(). The package only handles the originating model, not relationships — mismatched types here is the #1 failure mode.
Route model binding works unchanged. Because the primary key is a string UUID, Route::get('/posts/{post}', ...) and implicit binding resolve correctly out of the box. No customization needed.
Factories should never set id. Let the trait generate it:
Post::factory()->create(); // id auto-generated
Post::factory()->create(['id' => Str::uuid()]); // redundant, avoid
Layering with other traits. HasUuid composes fine with HasFactory, SoftDeletes, Notifiable, and observation traits — order in the use statement does not matter, but check that no other trait also overrides getKeyType() or getIncrementing(). If two traits declare the same method, PHP will fatal on trait conflict; resolve with insteadof.
Seeders and imports. When importing legacy rows that already have UUIDs, the trait will happily overwrite them unless you guard. Either use insert() (bypasses Eloquent boot hooks) or override the generation method to respect an already-set key (see Gotchas).
Verify the trait actually sets $incrementing = false and $keyType = 'string'. Many small UUID packages rely on you to do this. If your insert fails with "no such column: id" on SQLite or a cast error from MySQL, this is why — add both properties manually:
public $incrementing = false;
protected $keyType = 'string';
id must be in $fillable or $guarded must allow it if you ever mass-assign it. Otherwise create(['id' => ...]) silently drops it and the trait's generator takes over. Usually fine, but confusing during tests that seed specific IDs.
UUID versions matter for performance. v4 (random) fragments B-tree indexes on large tables. If the package uses Str::uuid() (v4), expect write amplification at scale; Str::orderedUuid() / UUIDv7 (Str::uuid7() on Laravel 11+) keeps inserts append-only. If the package does not offer a strategy switch, override the generation method in your model:
protected static function bootHasUuid(): void
{
static::creating(function (self $model) {
if (empty($model->{$model->getKeyName()})) {
$model->{$model->getKeyName()} = (string) Str::orderedUuid();
}
});
}
The if (empty(...)) guard is also what fixes the import/seeder overwrite problem above.
Generation happens on creating, not on instantiation. (new Post)->id is null until save. Never branch on $model->id before persisting — use $model->exists instead.
find() with a non-UUID string throws Laravel's standard ModelNotFoundException on PostgreSQL's uuid column type (invalid input syntax) rather than returning null. If you accept IDs from user input, validate with Str::isUuid() or a uuid rule before calling find().
SQLite and testing. The uuid blueprint type maps to varchar on SQLite, so most tests pass — but ordering assumptions that hold for auto-increment IDs do not. Use explicit ->orderBy('created_at') rather than relying on PK order.
Debugging. When a UUID is unexpectedly absent or wrong, inspect $model->getKeyName(), $model->getKeyType(), and $model->getIncrementing() at runtime. Also check Model::$booted — if the package's boot callback was registered once and the app caches models across tests, a stale trait can bite you on parallel test runs.
Extension point to prefer. Because the package is tiny and unmaintained (0 stars, no dependents), the safest long-term move is to copy the trait into your app/Models/Concerns directory and drop the dependency. You retain the exact behavior, gain the ability to customize UUID version and column handling, and are not exposed to a package that may vanish from Packagist. If you keep the dependency, pin it with an explicit version constraint and add a test asserting UUID generation so an accidental upgrade cannot silently break every write path.
How can I help you explore Laravel packages today?