skip to content

In Laravel 13, how do Eloquent's HasUuids and HasUlids traits change a model's primary key, and when is the key value assigned?

level: middleimportance: should knowfreq 45%

answer

  1. string key generated in PHP
  2. HasUuids uses Str::uuid7()
  3. ULID: 26 characters, lowercased
  4. no manual keyType or incrementing
  5. set during insert, before creating fires

basics

~20 s

HasUuids and HasUlids make the primary key a PHP-generated string, a UUIDv7 or a lowercase ULID, and report the key as a non-incrementing string. The value is assigned when the model is inserted, so a new unsaved instance has no key.

solid answer

~40 s

Both traits swap the auto-increment integer for a string generated in PHP. In Laravel 13 `HasUuids` calls `Str::uuid7()`, a time-ordered 36-character UUID, and `HasUlids` stores a lowercase 26-character ULID; `HasVersion4Uuids` keeps the older ordered version-4 generator. Through `HasUniqueStringIds` they override `getKeyType()` to return `'string'` and `getIncrementing()` to return `false`, so you do not set `$keyType` or `$incrementing` yourself. The value is generated in `performInsert()`, before the `creating` event fires and only if the column is empty, so `new Booking` has a null key until `save()`, while `Booking::create()` returns with the key set. Override `newUniqueId()` to change the generator, or `uniqueIds()` to give a generated value to a column other than the primary key.

go deeper

for a junior

Recall that adding HasUuids or HasUlids makes the model's key a generated string instead of an auto-increment integer.

for a middle

Explain which getters the traits override, when setUniqueIds() runs relative to the creating event, and how uniqueIds() moves the ID to another column.

for a senior

Choose between integer, UUIDv7 and ULID keys for index size, guessability and external consumers, and use a separate public ID column when joins should stay integer.

for a principal

Weigh a platform-wide identifier standard: what other services, logs and URLs consume, and the cost of migrating keys once data exists.

## What the traits are By default an Eloquent model's primary key is an auto-incrementing integer `id` produced by the database. Two traits in `Illuminate\Database\Eloquent\Concerns` replace that with a **string identifier generated in PHP**: - `HasUuids` generates a **UUID**: a 36-character identifier such as `0199a4c2-...`. In Laravel 13 it calls `Str::uuid7()`, so the value is a **version 7** UUID whose leading bits encode time, which keeps new keys roughly ordered and friendlier to database indexes. - `HasUlids` generates a **ULID**: a 26-character, lexicographically sortable identifier. The trait lowercases it, so the stored key looks like `01gd4d3tgrrfqeda94gdbtdk5c`. - `HasVersion4Uuids` is a third trait that keeps the older ordered version-4 generator (`Str::orderedUuid()`) for apps that depend on it. The primary-key column must be able to hold the string, so the migration declares a UUID or ULID column as the key rather than an auto-increment one. ## What changes on the model Both traits share `HasUniqueStringIds`, which overrides two getters for any column listed in `uniqueIds()`: | Method | Default model | With HasUuids / HasUlids | |---|---|---| | `getKeyType()` | `'int'` | `'string'` | | `getIncrementing()` | `true` | `false` | | `uniqueIds()` | `[]` | `[primary key name]` | So, unlike a hand-mapped string key, you do **not** also set `$keyType = 'string'` and `$incrementing = false`; the trait answers for them. ## When the ID is assigned The identifier is generated at **insert time**, not when the object is built. The order inside `performInsert()` is: 1. `setUniqueIds()` fills each column in `uniqueIds()` that is still empty, using `newUniqueId()`; 2. the `creating` event fires, so observers already see the key; 3. timestamps are set; 4. the row is inserted with a plain `insert()`, because the model is not incrementing. Consequences worth stating in an interview: - `$booking = new Booking([...]);` has a `null` key until `save()`. - `Booking::create([...])` returns a model whose key is already set, with no extra query to fetch it. - A key you set yourself before saving is kept, because only empty columns are filled. - Because the key exists before the insert, you can attach it to related records or a queued message in the same unit of work without waiting for the database. ## Customising - Override `newUniqueId()` to change the generator, for example to a random version-4 UUID. - Override `uniqueIds()` to choose the columns. Returning `['public_ref']` keeps an integer `id` primary key and gives a separate column a generated value, which is a common pattern for exposing a non-guessable reference in URLs while joins stay on small integer keys. ```php use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Eloquent\Model; class Booking extends Model { use HasUlids; public function uniqueIds(): array { return ['public_ref']; } } ``` ## Choosing between them - **UUIDv7**: the widest interoperability, a native type in some databases, 36 characters as text. - **ULID**: shorter (26 characters), case-insensitive alphabet, sortable by creation time. - **Auto-increment**: smallest and fastest to index, but guessable and assigned only by the database. None is always right; teams usually choose by what other systems consume the ID and whether the key appears in public URLs. ## Pitfalls - **Query-builder inserts skip the trait.** `Booking::insert([...])` is passed straight to the query builder, so no model is built, `newUniqueId()` never runs and the key column receives nothing. `upsert()` on an Eloquent builder does add generated IDs to rows that lack them. - **Foreign keys must match.** Tables that reference a UUID or ULID key need string-compatible foreign-key columns, not unsigned integers. - **Keys are strings in PHP.** Comparisons such as `$booking->id === 5` never match, and a key received from outside should be validated as a UUID or ULID before it is used in a lookup. - **Adding the trait to an existing integer-keyed model** changes the key type Eloquent reports but does not convert existing rows; the schema and data have to be migrated as well.

  • How would you keep an integer primary key but expose a ULID in public URLs with Eloquent?
    Use `HasUlids` and override `uniqueIds()` to return `['public_ref']`. The trait then fills `public_ref` on insert, while `id` stays an auto-incrementing integer because it is no longer in `uniqueIds()`. Joins and foreign keys keep using the compact integer, and URLs use the non-guessable ULID column.
  • Does `HasUuids` overwrite a UUID you assign before saving?
    No. `setUniqueIds()` only fills a unique-id column when it is empty, so a key you set explicitly, for example one received from another system, is inserted as given.

saying these in an interview costs you the question

  • HasUuids needs $keyType = 'string' and $incrementing = false added by hand
  • The UUID is generated as soon as the model is instantiated
  • HasUlids produces 36-character dashed identifiers
  • The database generates the UUID through a column default
  • HasUuids in Laravel 13 generates random version-4 UUIDs