Introduction
UUIDs (Universally Unique Identifiers) are 128-bit identifiers that are globally unique, making them ideal for scenarios where you want to hide sequential ID patterns from your users. This article walks you through converting an existing Laravel application from auto-incrementing integer IDs to UUIDs.
Why Use UUIDs?
Pros:
– Security: Prevents ID enumeration attacks (users can’t guess `/users/1`, `/users/2`), and can’t guess the first user which is usually the system admin.
– Distributed Systems: Generate IDs client-side without database coordination
– Merging Data: No ID conflicts when merging databases from different sources
– API Design: Cleaner, more secure public-ready identifiers
– Scalability: Better for microservices and distributed architectures
Trade-offs (There is always a trade-off):
– Storage: UUIDs take 36 characters (string) vs integers (typically 4-8 bytes)
– Performance: A bit slower indexing compared to sequential integers, specially when searching data.
– Readability: Less human-friendly than sequential numbers
Implementation Steps
Step 1: Create a UUID Trait
First, create a reusable trait that handles UUID generation automatically:
<?php
namespace App\Http\Traits;
use Illuminate\Support\Str;
trait UseUuid
{
protected static function booted(): void
{
static::creating(function ($model) {
$model->incrementing = false;
$model->keyType = 'string';
$model->{$model->getKeyName()} = (string) Str::uuid();
});
}
public function getIncrementing()
{
return false;
}
public function getKeyType()
{
return 'string';
}
}
What is these built-in function does:
– `booted()`: Hooks into the model lifecycle to generate UUIDs before creation
– `getIncrementing()`: Tells Laravel this model doesn’t use auto-incrementing IDs
– `getKeyType()`: Specifies the primary key is a string, not an integer
Step 2: Update Your Models
Add the trait to all models that need UUIDs:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Http\Traits\UseUuid;
class Post extends Model
{
use UseUuid;
protected $fillable = [
'user_id',
'title',
'content',
'status',
];
public function user()
{
return $this->belongsTo(User::class);
}
}
Apply the same trait to any other models that need it.
Step 3: Update Migration Files
This is the main and important step. You need to update your migration files to use `uuid()` instead of `id()`:
Normally you had Before (without using UUID, Integer IDs):
Something like this:
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('content');
$table->timestamps();
});
After (when using UUID):
Schema::create('posts', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('content');
$table->timestamps();
});
Step 4: For Pivot Tables:
Don’t forget pivot tables for many-to-many relationships:
You should change it to be something like below
Schema::create('post_tag', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('post_id')->constrained()->cascadeOnDelete();
$table->foreignUuid('tag_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
Step 6: Fresh Migration
Since you’re changing the fundamental structure, run a fresh migration (DON’T DO IT ON PRODUCTION OR STAGING THIS WILL DROP ALL TABLES) when you finish.
php artisan migrate:fresh
Testing Your Implementation:
// Create a user
$user = User::create([
'name' => 'Mohcin',
'email' => 'mochin@example.com',
'password' => bcrypt('password')
]);
// Check the ID
var_dump($user->id);
// Output: string(36) "3800f3d7-139c-433a-9e6f-a0947411f7a6"
// Create a post with relationship
$post = Post::create([
'user_id' => $user->id,
'title' => 'My First Post',
'content' => 'Hello World!'
]);
var_dump($post->id);
// Output: string(36) "2bebf865-184f-40b6-aabd-426efa0dd833"
// Test relationship
$post->user->name; // "Mohcin"
UUIDs provide better security and scalability for modern applications, especially those with public APIs. While there are trade-offs in storage and performance, the benefits often outweigh the costs for most applications.
Resources: