Skip to content

Instantly share code, notes, and snippets.

@ZeroGodForce
Last active April 17, 2024 06:30
Show Gist options
  • Save ZeroGodForce/c2f69d9083ed4fa03a09b8278e09861a to your computer and use it in GitHub Desktop.
Save ZeroGodForce/c2f69d9083ed4fa03a09b8278e09861a to your computer and use it in GitHub Desktop.
HasUuid Trait for Laravel

HasUuid Trait for Laravel

Credits

This trait his heavily inspired by and based on the following implementations of UUID generation traits:

Basics

The purpose of this trait is to add a lightweight easy way to make use of UUIDs in Laravel applications without the need to manually specify their creation or search. Features include:

  • Automagically generates UUIDs for models when they are being created
  • In development and testing environments it will create semi-readable UUID's; prefixing the UUID with the model's name. This can be useful when evaluating raw model data or URLs that feature multiple UUIDs from different models.
  • Can be set to create "time ordered" UUIDs, which provides a similar benefit to incrementing integer IDs without the security/ambiguity risk

Usage

Add the hasUuid trait to your model:

  use HasUuid;

By default, UUIDs will automatically be generated for any model that has a field called 'uuid'.
To use a custom field name is different, you need to specify the following property:

/**
* Custom UUID field name
*
* @var string
*/
protected uuidFieldName = 'uuid_fieldname';

To use a UUID as a primary key, make sure the model has the following properties specified:

YourModel.php

/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = false;

/**
* The primary key's datatype.
*
* @var string
*/
protected $keyType = 'string';

/**
* The ID field is a UUID
*
* @var bool
*/
protected bool $primaryKeyIsUuid = true;

Known issues

  • On PostgreSQL databases, this trait can throw errors in non-production environments as PostgreSQL doesn't support underscores in it's UUID column type. As a workaround/fix, it was replaced with the dash (on line 80). However this resolution has not been fully tested yet.
<?php
namespace App\Traits;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\App;
trait HasUuid
{
/**
* This trait his heavily inspired by and based on the following implementations of UUID generation traits:
*
* Laravel-UUID: https://github.com/binarycabin/laravel-uuid
* UuidModel Trait: https://gist.github.com/danb-humaan/b385ef92ed2336fd5d12
* The mysterious “Ordered UUID” by
* Italo Baeza Cabrera: https://itnext.io/laravel-the-mysterious-ordered-uuid-29e7500b4f8
*/
public static function boot()
{
parent::boot();
/**
* Listens for a model creation event and generates a UUID for the desired UUID column
*/
static::creating(function ($model) {
$uuidFieldName = $model->getUuidFieldName();
$useTimeOrderedUuid = $model->getUseTimeOrderedUuid();
if (empty($model->{$uuidFieldName})) {
if (App::environment(['local', 'testing'])) {
$model->{$uuidFieldName} = static::generateReadableUuidForTesting($model);
} else {
$model->{$uuidFieldName} = ($useTimeOrderedUuid) ?
static::generateTimeOrderedUuid()
:
static::generateUuid();
}
}
});
/**
* Listens for a model saving event to prevent a UUID from being changed.
* TODO: TEST TO SEE WHETHER THIS OBSTRUCTS IF A MODEL IS SAVED MANUALLY RATHER THAN BEING CREATED
*/
static::saving(function ($model) {
$uuidFieldName = $model->getUuidFieldName();
$originalUuid = $model->getOriginal($uuidFieldName);
if (!empty($originalUuid)) {
if ($originalUuid !== $model->{$uuidFieldName}) {
$model->{$uuidFieldName} = $originalUuid;
Log::warning('Attempt to change existing UUID blocked');
}
}
});
}
/**
* Static call to search for a record via the UUID
*
* @param $uuid
*
* @return mixed
*/
public static function findByUuid($uuid)
{
return static::byUuid($uuid)->first();
}
/**
* Generates a test UUID with the model name as a prefix for easy distinction when testing
*
* @param $model
*
* @return string
*/
public static function generateReadableUuidForTesting($model): string
{
$className = strtolower(class_basename($model)) . '-';
$numToRemove = strlen($className);
$remaining = (36 - (int) $numToRemove);
return $className . Str::substr(static::generateUuid(), $numToRemove, $remaining);
}
/**
* Generates a "Time Ordered" UUID which is generated in conjunction with the server timestamp. Less unique, but
* useful if ordering by time is important
*
* @return string
*/
public static function generateTimeOrderedUuid(): string
{
return (string) Str::orderedUuid();
}
/**
* Generates a standard version 4 UUID
*
* @return string
*/
public static function generateUuid(): string
{
return (string) Str::uuid();
}
/**
* Checks to see if "Time Ordered" UUIDs have been specified
*
* @return bool
*/
public function getUseTimeOrderedUuid(): bool
{
if ($this->useTimeOrderedUuid) {
return true;
}
return false;
}
/**
* Check to see if a specific column name has been specified for the UUID
*
* @return string
*/
public function getUuidFieldName(): string
{
if ($this->primaryKeyIsUuid) {
return $this->getKeyName();
}
if (!empty($this->uuidFieldName)) {
return $this->uuidFieldName;
}
return 'uuid';
}
/**
* Scoping method to search for a record via the UUID
*
* @param $query
* @param $uuid
*
* @return mixed
*/
public function scopeByUuid($query, $uuid)
{
return $query->where($this->getUuidFieldName(), $uuid);
}
}
@ZeroGodForce
Copy link
Author

@LiamKarlMitchell @alexrafuse Nicely spotted guys, thanks for your input and feedback.

Finally got a chance to look into the problem and (long story short), I've realised that when you define uuid as the column type in migrations, Laravel creates a CHAR field for MySQL (which is why it accepts anything), but a an actual UUID column type for PostgreSQL - and that appears to store its UUIDs as bigInt. This being the case (and model names including more letters than hexadecimal has) it gives out a bigInt error when trying to write it as a string.

My current idea is to try and generate a predictable hex prefix that can be used in a similar way to the model name prefix currently working on MySQL but the alternative solutions I've tried so far don't work yet. Will update again when I have something but will probably add a property in the next day or so to just disable the custom uuid in the meantime.

Thanks again for the feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment