Generating UUID and ApiTokens for Laravel Eloquent Models using Observers cover image

Generating UUID and ApiTokens for Laravel Eloquent Models using Observers

Roberto Gardenier • March 20 - 2017

how to tutorial php api token laravel observers uuid
This article was written quite some time ago and might not be relevant anymore. Please be aware of this before extracting and/or applying anything from this article.

Recently I needed to replace Eloquent's default auto-increment numeric ID with a UUID on a few of my models. This can be done in several ways, but after a good chat with the great people in the #Laravel channel on PHPNL's Slack team, I decided to use Laravel's Observers. Also, I needed to generate API Tokens for certain models, so why not use the same solution.

Some notes on this implementation:

  • UUID is short for “universal unique identifier” which is a set of 36 characters, containing letters and numbers. It is better to use a UUID as identifier for external accessible entities. This prevents guessing the next id, sort of security through obscurity measure.
  • We will hook the Observer to act on Eloquent Model Events.
  • Instead of using the Ramsey's UUID package you can always choose to use the Laravel-UUID package from Webpatser instead.
  • This example is based on Laravel 5.4, but the outlines of this implementation should apply to earlier versions as well.

Having that said: let's get started!

The Migration

For the sake of convenience I will use a model named Consumer. My Migration file looks like the following:

/**
 * Run the migrations.
 *
 * @return void
 */
public function up()
{
    Schema::create(
        'consumers',
        function (Blueprint $table) {
            $table->engine = 'InnoDb';
            $table->uuid('id');
            $table->char('api_token', 64)->nullable();
            $table->string('name');
            $table->string('url');
            $table->timestamps();
            $table->primary('id');
        }
    );
}

Make sure to tell Eloquent to define a different type of primary key for the Consumers table, as done with:

$table->uuid('id');
$table->primary('id');

Making the UUID our primary key means it will be indexed, and making our records be uniquely identifiable. This way we know for sure that the UUID, which already should be unique, does not collide with other generated UUID's. Although the chance of collision might be small, it's a good practice nonetheless. Having a indexed primary key also gives us the ability to JOIN to other tables.

The UUIDModel

This model will be used as an abstract to create our other models from. Make sure to disable the auto-increment on your primary key field by adding:

public $incrementing = false;

My UUIDModel looks like this, and is in App/Models/UUIDModel.php

namespace App\Models;

use App\Observers\UUIDModelObserver;
use Illuminate\Database\Eloquent\Model;

/**
 * Class UUIDModel
 *
 * @property string $id
 * @package App\Models
 */
abstract class UUIDModel extends Model
{
    /**
     * Disable auto-incrementing the primary key field for this model.
     *
     * @var bool $incrementing
     */
    public $incrementing = false;

    /**
     * Override the primary key type.
     *
     * @var string $keyType
     */
    protected $keyType = 'string';

    /**
     * Add UUIDModelObserver to the boot method of the current model.
     */
    public static function boot()
    {
        parent::boot();
        self::observe(UUIDModelObserver::class);
    }
}

The UUIDModelObserver

Create the observer in App/Observers/UUIDModelObserver.php:

namespace App\Observers;

use App\Helpers\ModelHelper;
use App\Models\UUIDModel;

/**
 * Class UUIDModelObserver
 *
 * @package App\Observers
 */
final class UUIDModelObserver
{
    /**
     * @param UUIDModel $model
     *
     * @return null
     */
    public function creating(UUIDModel $model)
    {
        $model->{$model->getKeyName()} = ModelHelper::generateUuid();

        return null;
    }
}

The ConsumerModelObserver

Create the observer in App/Observers/ConsumerModelObserver.php:

namespace App\Observers;
use App\Helpers\ModelHelper;
use App\Models\Consumer;

/**
 * Class ConsumerObserver
 *
 * @package App\Observers
 */
class ConsumerModelObserver
{
    /**
     * @param Consumer $model
     *
     * @return null
     */
    public function created(Consumer $model)
    {
        $model->{$model->getApiTokenKey()} = ModelHelper::generateApiToken($model);

        return null;
    }
}

The ModelHelper

Create the helper in App/Helpers/ModelHelper.php

namespace App\Helpers;

use App\Models\Consumer;
use Ramsey\Uuid\Uuid;

class ModelHelper
{
    /**
     * Generates UUID4
     *
     * @return string
     */
    public static function generateUuid()
    {
        return Uuid::uuid4()->toString();
    }

    /**
     * @param Consumer $consumer
     *
     * @return string
     */
    public static function generateApiToken(Consumer $consumer)
    {
        return hash_hmac('sha256', strtolower(trim($consumer->{$consumer->getKeyName()} . $consumer->{$consumer->getUpdatedAtColumn()})), config('app.key'));
    }
}

The (Consumer) Model

Create the model in App/Models/Consumer.php

namespace App\Models;

use App\Observers\ConsumerModelObserver;

class Consumer extends UUIDModel
{
    /**
     * The API token field.
     *
     * @var string $apiTokenKey
     */
    private $apiTokenKey = 'api_token';

    /**
     * @var array
     */
    public $fillable = [
        'name',
        'url',
        'api_token',
        'ip',
        'active',
    ];

    /**
     * Add the ConsumerModelObserver to the boot method of the current model.
     */
    public static function boot()
    {
        parent::boot();
        self::observe(ConsumerModelObserver::class);
    }

    /**
     * Return the API token field
     *
     * @return string
     */
    public function getApiTokenKey()
    {
        return $this->apiTokenKey;
    }
}

The breakdown

When Eloquent instantiates a model, it fires several model events. Inside our UUIDModelObserver we act on the creating event. The current model is supplied when calling this method and makes it possible to change attributes. In this case we ask for the model's primary key field and supply it with a freshly baked UUID and API token from, using our ModelHelper.

So in short:

  1. The Consumer class extends the UUIDModel class.
  2. The UUIDModel contains a boot() method where it subscribes the UUIDModelObserver.
  3. The UUIDModelObserver acts on the creating event from the UUIDModel and supplies a UUID from the ModelHelper to the UUIDModel primary key field, and a API token.

The Tinker

$ php artisan tinker
Psy Shell v0.8.3 (PHP 7.0.10 ÔÇö cli) by Justin Hileman
>>> $consumer = App\Models\Consumer::create(['name' => 'Caroga\'s Blog', 'url' => 'blog.caroga.net']);
=> App\Models\Consumer {#665
     name: "Caroga's Blog",
     url: "blog.caroga.net",
     id: "7a8fd07c-8e6c-4e9b-99fe-7f487387b8e8",
     updated_at: "2017-03-20 17:03:06",
     created_at: "2017-03-20 17:03:06",
     api_token: "24a4e39c801d26936833891900d170b9f954792bd9241020d5c541829a368811",
   }

As you can see, the ID field has been supplied with a UUID and the API token has been generated as well.

Conclusion

Using Laravel's Observers and Eloquent Model Events, it gets very easy to add or change attributes in several stages of the model's lifecycle. This way you don't have to introduce anymore logic then needed on the models where you wish to add these attributes.

Remarks

  1. Make sure that your observers do not return an object or any other value than null if you are not planning on halting after your observer. Laravel's event dispatcher is built to halt if you return anything other than null. I've created a pull request to address this a little more in the documentation.
  2. Using Traits as suggested by many other resources on the internet is also a possible implementation for UUIDs in models, but offer less flexibility when stacking events like I needed above. Therefore, using observers was a much cleaner approach.
  3. You can find a working copy of this setup at my github page.

Thank you @NetAnts and @markredeman for helping me find this approach.

This article was written quite some time ago and might not be relevant anymore. Please be aware of this before extracting and/or applying anything from this article.

Like it? Why not share it:

About the author

Profile Picture Caroga

I am the Co-Founder and continuous Sponsor of 010PHP. Also a regular Community Supporter and PHP Enthusiast. I don't just like to express my creativity as a Developer, but also as a Musician and Photographer.

Caroga's profile image

Roberto Gardenier a.k.a. 'Caroga'


Founder and Continuous Sponsor of @010PHP. PHP Community Supporter, Hobby Musician and Photographer, New to Videography. Freelance Developer. Born in Brazil.