---
title: "Model traits — Package Toolkit for Laravel | Roundly"
description: "HasConfigurableKey drives a package model’s own bigint/uuid/ulid key from config; StoresUtcTimestamps writes its timestamps in UTC."
url: https://roundly-consulting.com/open-source/docs/package-toolkit-for-laravel/model-traits
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Package Toolkit for Laravel](https://roundly-consulting.com/open-source/docs/package-toolkit-for-laravel.md)

# Model traits

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():

```php
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';
    }
}
```

```php
// config/tickets.php
return [
    'primary_key_type' => 'bigint',   // bigint | uuid | ulid — the type of the package's own ids
];
```

```php
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\_type | Column (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.

| Member | What it does |
| --- | --- |
| `abstract protected primaryKeyTypeConfigKey(): string` | You implement it: the config key naming the type. |
| `configuredKeyType(): KeyType` | The configured type, read with KeyType::fromConfig(): not set is bigint, ' ULID ' reads as ulid, a typo throws. |
| `newUniqueId(): string` | Str::uuid7() on uuid, Str::ulid() on ulid. Never called on bigint — the database mints those. |
| `initializeHasUniqueStringIds(): void` | Switches Laravel’s HasUniqueStringIds off on bigint. |
| `isValidUniqueId(mixed $value): bool` | Protected. 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:

```php
// 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:

```php
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 UTC | Stored created\_at |
| --- | --- |
| Without the trait (Laravel’s default) | 2026-03-08 01:30:15 — the host’s wall time |
| With StoresUtcTimestamps | 2026-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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/package-toolkit-for-laravel.md)
