Using UUIDs with Laravel’s Eloquent ORM

By default, Eloquent uses an auto-incrementing integer as the primary key for its tables. While most of the time this is totally acceptable, sometimes there is a need for primary keys to be less predictable.

Example: Table Reservations

You are writing an application for table reservations at a restaurant. You would likely have a database table that holds those reservations and ties them to a physical table and user account.

CREATE TABLE `reservations` (
    `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    `user_id` int(11) unsigned NOT NULL,
    `table_id` int(11) unsigned NOT NULL,
    `reservation_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    PRIMARY KEY (`id`),
    KEY `user_id` (`user_id`),
    KEY `table_id` (`table_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

If you were to create a way to display existing reservations at a given URL, say http://acmereservations.com/reservation/15 where 15 refers to the `reservations`.`id` field, you can see how it would be very easy to simply bump that number up to 16 and see someone else’s reservation and perhaps even cancel it. For the sake of simplicity we’ll ignore the fact that some authentication and access control can prevent this from happening.

Why UUID?

UUID is a uniquely generated 36 character string and looks something like: 22beb489-2ba9-44c8-b189-5855e1d4d1ad. While I say that the UUID is unique, that’s not entirely true. Let’s just say the likelihood of duplicates is extremely small. Along with being unique, it’s also very unpredictable. You can see how it would be much harder to guess other reservation IDs given a URL of http://acmereservations.com/reservation/22beb489-2ba9-44c8-b189-5855e1d4d1ad.

Secondarily, UUIDs allow site owners to mask the number of records in their database tables. If your registration ID is 15 you could assume there are only 14 other reservations and maybe this isn’t the best place to eat. A UUID will mask the number of records in a table with the generated string.

So how do we use UUIDs with Eloquent?

It turns out it’s not overly difficult. I would suggest the best way is for all Eloquent models needing a UUID to extend a base model that implements the following functionality:

<?php

/**
 * This is a great UUID generator package available on Composer
 * but you can generate your UUID however you see fit.
 */
use Rhumsaa\Uuid\Uuid;

class UuidModel extends Eloquent
{
    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = false;

    /**
     * The "booting" method of the model.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        /**
         * Attach to the 'creating' Model Event to provide a UUID
         * for the `id` field (provided by $model->getKeyName())
         */
        static::creating(function ($model) {
            $model->{$model->getKeyName()} = (string)$model->generateNewId();
        });
    }

    /**
     * Get a new version 4 (random) UUID.
     *
     * @return \Rhumsaa\Uuid\Uuid
     */
    public function generateNewId()
    {
        return Uuid::uuid4();
    }
}

Now, all you have to do is extend the UuidModel class for your models and the UUID will be set automatically on the primary key before creation.

It’s worth noting that you will need to change your database schema to accommodate the UUID. For our `reservations` table, we would revise it like so:

CREATE TABLE `reservations` (
    `id` char(36) NOT NULL DEFAULT '',
    `user_id` int(11) unsigned NOT NULL,
    `table_id` int(11) unsigned NOT NULL,
    `reservation_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
    PRIMARY KEY (`id`),
    KEY `user_id` (`user_id`),
    KEY `table_id` (`table_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

And, that’s actually it! Thanks to r15ch13 for the vast majority of the code involved. If you have any thoughts or a different way to approach this, let me know in the comments below.

Update [1/16/2014]:

It’s worth updating here that I did have some issues when running tests with PHPUnit. The first test would always work as expected, however, the second test wouldn’t add in the UUID on the primary key. I don’t know what is going on here, but it seems to have something to do with PHPUnit since it worked fine when run inside of Laravel. To get around the issue I pulled the event declarations out of the UuidModel and put them into a dedicated events.php file like so:

<?php

    $models = ['Reservation', 'User', 'Table'];

    foreach ($models as $model) {
        $model::creating(function ($model) {
            $model->{$model->getKeyName()} = (string)Uuid::uuid4();
        });
    }

I then revised the UuidModel as follows:

<?php

class UuidModel extends \Eloquent
{
    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = false;

}

Update [11/25/2015]:

As was suggested in the comments below, a Trait is a perfect solution to clean up our models and consolidate our code.

<?php

namespace App;

use Ramsey\Uuid\Uuid;

trait UuidForKey
{
    /**
     * Boot the Uuid trait for the model.
     *
     * @return void
     */
    public static function bootUuidForKey()
    {
        static::creating(function ($model) {
            $model->incrementing = false;
            $model->{$model->getKeyName()} = (string)Uuid::uuid4();
        });
    }

    /**
     * Get the casts array.
     *
     * @return array
     */
    public function getCasts()
    {
        return $this->casts;
    }
}

I then revised the UuidModel as follows:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Reservation extends Model
{
    use UuidForKey;

}