Recently I had to implement Universally unique identifiers (UUIDs) in Laravel 7, and ran into some problems that I hope this post clears up for others who are doing the same.
High-level reasons for using UUIDs
A) They remove numbered IDs from your URLs, so users cannot see how many of a certain object your app has created. E.g.
https://myapp.com/api/users/5
vs.
https://myapp.com/api/users/0892b118-856e-4a15-af0c-66a3a4a28eed
B) They make IDs a lot harder to guess. This is good for security, but we probably should be implementing other techniques to guard against this.
Implementing UUIDs as primary keys
How to change your database migrations
Firstly, you replace your current auto-incrementing integer ID fields with UUIDs in your database migrations. You could also follow the route of keeping auto-incrementing IDs and implementing UUIDs as an additional field on your tables that will be used when exposing URLs to users (in which case you make the ID field hidden in your models), but that is not what we are doing here. Let's see what this would look like for a hypothetical employees table.
public function up()
{
Schema::create('employees', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('name');
$table->string('email')->unique();
$table->string('work_location')->nullable();
$table->timestamps();
});
}
Here, notice we replaced the normal id(); declaration with uuid(); and made it the primary key.
Let's make that a Trait
Next, we can implement a Laravel lifecycle hook to make sure that a UUID is assigned when a new instance of this model is created. We could code this directly in the model, but if you're going to use UUIDs on more than one model, I suggest using a Trait instead (Kudos to Wilbur Powery who I learned this from in this Dev post). A trait basically allows you to create functionality, and use that functionality in more than one model by calling it with the use keyword.
To create a new Trait, create a \App\Http\Traits\ folder (only my preference, you can put it somewhere else too), and also a new file for the Trait. We will call the file UsesUuid.php.
Here is the code for the trait:
<?php
namespace App\Http\Traits;
use Illuminate\Support\Str;
trait UsesUuid
{
protected static function bootUsesUuid() {
static::creating(function ($model) {
if (! $model->getKey()) {
$model->{$model->getKeyName()} = (string) Str::uuid();
}
});
}
public function getIncrementing()
{
return false;
}
public function getKeyType()
{
return 'string';
}
}
Here, we use the \Illuminate\Support\Str class to easily generate UUIDs. The getIncrementing() method tells Laravel that the IDs on this model will not be incrementing (as we are returning false), and the getKeyType() method tells Laravel the primary key will be of type string. The bootUsesUuid() method allows us to tap the power of Laravel lifecycle hooks. You can learn more about those here. Basically, our code is telling Laravel that when a new instance of this model is being created, generate a UUID primary key for it!
Now, we can easily implement this trait on a model with the use keyword.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
...
class Employee extends Model
{
...
use \App\Http\Traits\UsesUuid;
...
}
Referencing UUIDs as foreign keys
To reference a UUID on a table as a foreign key, you simply change the type of the foreign key field on your table. For example, this...
Schema::create('another_table', function(Blueprint $table) {
$table->id();
$table->unsignedBigInteger('employee_id');
$table->string('some_field');
$table->foreign('employee_id')
->references('id')
->on('shifts')
->onDelete('cascade');
});
... where we created a unsignedBigInteger to reference the employee_id foreign key, changes to this:
Schema::create('another_table', function(Blueprint $table) {
$table->id();
$table->uuid('employee_id');
$table->string('some_field');
$table->foreign('employee_id')
->references('id')
->on('shifts')
->onDelete('cascade');
});
Easy as that! One more thing though...
UUIDs and polymorphic relationships
You might find that your model gets referenced in polymorphic relationships, either by your own doing or by a package that you are pulling in. The table might look something like this in the migration:
public function up()
{
Schema::create('some_package_table', function (Blueprint $table)
{
$table->bigIncrements('id');
$table->morphs('model');
...
}
}
Here, the morphs() method is going to create two fields in your database, namely model_id of type unsignedBigInteger and model_type of type string. The problem is that our model is now using a UUID instead of the incrementing integer ID, so this is going to give you and error that says something like:
Data truncated for column 'model_id' at row 1
We need the model_id field to now support our new UUID which is of type CHAR(36). Not to worry! Laravel makes this super easy, and you don't have to do that manually. Simply change the migration to this:
public function up()
{
Schema::create('some_package_table', function (Blueprint $table)
{
$table->bigIncrements('id');
$table->uuidMorphs('model');
...
}
}
Another reason to love Laravel! Happy coding!
Top comments (7)
I used a fresh Laravel 8.x installation and everything works fine, but as I implemented this to the User model too, the login doesn't work any more. Just tried to create a new user from /register and after registration, normally it redirects to /dashboard as being logged in, but instead redirects to /login and I tried with the credentials I`ve just used for registration, but it fails without any errors.
Is it just because it's Laravel 8.0 and not 7.x ?
Hi Cornel, thanks so much for this. I had an issue though and it was due to the first line in the migration file
$table->uuid('id')->primary;
. I had to change it to$table->uuid('id')->primary();
to get it working. Theprimary
modifier has bracketsHi Ibrahim. Thanks so much for bringing this up, I'll make the required updates.
Hi Cornel, thanks for this tutorial but i have problem...if i inserted in the user_id an id is not in users table..he's accepting the id i dont know why..i thought if we used references and foreign after inserting will check if this user_id is existing in users table in id
do you have any solves to this problem
Great post !
A possible optimization, wouldn't using
Str::orderedUuid()
when setting up the trait be better in terms of indexing?why would not to override variables?
protected $keyType = 'string';
public $incrementing = false;
Some comments may only be visible to logged-in visitors. Sign in to view all comments.