UUIDs in Laravel 7

UUIDs in Laravel 7

cverster profile imageCornel VersterMay 16 Updated on Jul 07, 2020 ・4 min read

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!

posted on 2020-10-31 00:26  刘应杰  阅读(66)  评论(0编辑  收藏  举报

导航