DEV Community

Cornel Verster
Cornel Verster

Posted on • Edited on

UUIDs in Laravel 7

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.

FilePath

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)

Collapse
 
spidey89 profile image
Catalin Farcas • Edited

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 ?

Collapse
 
tosinibrahim96 profile image
Ibrahim Alausa

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. The primary modifier has brackets

Collapse
 
cverster profile image
Cornel Verster

Hi Ibrahim. Thanks so much for bringing this up, I'll make the required updates.

Collapse
 
yaman99 profile image
yaman99

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

Collapse
 
th3error profile image
Abdelrahman Moussa

Great post !
A possible optimization, wouldn't using Str::orderedUuid() when setting up the trait be better in terms of indexing?

Collapse
 
narkoze profile image
Edgars

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.