NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Two traits for your package’s own Eloquent models: one makes the model’s primary key follow config, the other writes its timestamps in UTC whatever the host’s timezone.

HasConfigurableKey

RoundlyConsulting\PackageToolkit\Concerns\HasConfigurableKey drives a model’s own primary key from a <pkg>.primary_key_type config key, so the model matches the column your migration emitted for the same value. The model names the key in primaryKeyTypeConfigKey():

use Illuminate\Database\Eloquent\Model;
use RoundlyConsulting\PackageToolkit\Concerns\HasConfigurableKey;

final class Ticket extends Model
{
    use HasConfigurableKey;

    protected function primaryKeyTypeConfigKey(): string
    {
        return 'tickets.primary_key_type';
    }
}
// config/tickets.php
return [
    'primary_key_type' => 'bigint',   // bigint | uuid | ulid — the type of the package's own ids
];
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use RoundlyConsulting\PackageToolkit\Enums\KeyType;

// The migration emits the matching column from the same key.
$primary = KeyType::fromConfig('tickets.primary_key_type');

Schema::create('tickets', function (Blueprint $table) use ($primary): void {
    match ($primary) {
        KeyType::BigInt => $table->id(),
        KeyType::Uuid => $table->uuid('id')->primary(),
        KeyType::Ulid => $table->ulid('id')->primary(),
    };
    // …
});
<pkg>.primary_key_typeColumn (your migration)Model
Not set, or 'bigint'$table->id()Auto-increment, getKeyType() = int — as if the trait were absent.
'uuid'$table->uuid('id')->primary()A time-ordered UUIDv7 minted when the model is first saved, getKeyType() = string, not incrementing.
'ulid'$table->ulid('id')->primary()A ULID minted when the model is first saved, getKeyType() = string, not incrementing.
Anything else ('uiid')—Throws InvalidConfigurationException when the model is instantiated.

This is the inbound key: the package’s own id, which other packages’ polymorphic columns point at. It is distinct from the outbound key — <pkg>.key_type, read with KeyType::fromConfig() — the type of the host models your package points at (see Key types & schema macros). The two are configured separately.

MemberWhat it does
abstract protected primaryKeyTypeConfigKey(): stringYou implement it: the config key naming the type.
configuredKeyType(): KeyTypeThe configured type, read with KeyType::fromConfig(): not set is bigint, ' ULID ' reads as ulid, a typo throws.
newUniqueId(): stringStr::uuid7() on uuid, Str::ulid() on ulid. Never called on bigint — the database mints those.
initializeHasUniqueStringIds(): voidSwitches Laravel’s HasUniqueStringIds off on bigint.
isValidUniqueId(mixed $value): boolProtected. A ULID on ulid; a UUID of any version otherwise, so an id the host set explicitly still resolves.

The type is read with KeyType::fromConfig(): not set is bigint, the value is trimmed and matched case-insensitively, and a typo throws as soon as the model is instantiated rather than building the wrong key:

// config('tickets.primary_key_type') === 'uiid' — a typo
new Ticket;
// throws InvalidConfigurationException:
// Configuration value [tickets.primary_key_type] must be one of [bigint, uuid, ulid] (case-insensitive), [uiid] given.
  • How it works: Laravel’s HasUniqueStringIds drives getKeyType(), getIncrementing(), uniqueIds() and route-binding validation from one usesUniqueIds flag. The trait sets that flag in its initializer, so on bigint the model behaves exactly as if it had never used the trait.
  • Time-ordered keys: UUIDv7 and ULID both sort by creation time, so latest('id') and id-ordered chunks keep creation order.
  • Route binding refuses a malformed key before SQL. On uuid and ulid, a route key that is not a UUID (or a ULID) throws ModelNotFoundException — a 404 — instead of reaching the database. That matters on PostgreSQL, where a native uuid column answers 'not-a-key' with invalid input syntax for type uuid, a 500. A ULID sent to a uuid model, and a UUID sent to a ulid model, are refused the same way.
  • An id you set yourself is kept: a key is minted only when id is empty.

StoresUtcTimestamps

RoundlyConsulting\PackageToolkit\Concerns\StoresUtcTimestamps makes a model’s “now” UTC: created_at / updated_at, touch(), soft deletes, and anything else Eloquent writes through freshTimestamp() / freshTimestampString(). A host on America/New_York then stores the same instants as a host on UTC:

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use RoundlyConsulting\PackageToolkit\Concerns\StoresUtcTimestamps;

final class Delivery extends Model
{
    use SoftDeletes;
    use StoresUtcTimestamps;
}
Host on America/New_York, at 06:30:15 UTCStored created_at
Without the trait (Laravel’s default)2026-03-08 01:30:15 — the host’s wall time
With StoresUtcTimestamps2026-03-08 06:30:15
  • Only the zone changes. freshTimestamp() returns Date::now('UTC'): the same instant, with its microseconds, in the host’s date class — Date::use(CarbonImmutable::class) gives a CarbonImmutable. The column’s precision is still decided by the model’s date format.
  • Read the columns back as UTC too. The trait writes; it does not change how Eloquent reads. The default datetime cast parses a stored 06:30:15 in app.timezone, which is the wrong instant on a non-UTC host — cast the columns with a cast that parses in UTC, or run the host on app.timezone = UTC.

Both traits are tested on SQLite and on PostgreSQL: bigint, uuid and ulid keys, the route-key refusal against a native uuid column, and the UTC round trip.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.