skip to content

In Laravel, when would you pick Str::ulid() or Str::uuid7() over Str::uuid() for order identifiers, and what do they return?

level: middleimportance: should knowfreq 35%

answer

  1. uuid() is random version 4
  2. uuid7() and ulid() start with time
  3. objects, not strings: cast them
  4. 36 characters versus 26
  5. sortable ids leak creation time

basics

~10 s

Str::uuid() gives a random version-4 UUID; Str::uuid7() and Str::ulid() start with a timestamp, so new IDs sort in creation order and index better. All three return objects, so cast them to strings.

solid answer

~40 s

`Str::uuid()` returns a random **version 4** `Ramsey\Uuid\UuidInterface`; `Str::uuid7($time = null)` returns a **version 7** UUID whose leading bits are a millisecond timestamp; `Str::ulid($time = null)` returns a `Symfony\Component\Uid\Ulid` — 26 Crockford Base32 characters, also time-first. `Str::orderedUuid()` is Laravel's older timestamp-first variant. Pick a time-ordered one for primary keys and order numbers, because new values land at the end of the index instead of at random positions; pick `uuid()` when an ID must reveal nothing about when it was made. All of them return **objects**, so `(string) Str::ulid()` before storing or comparing. In Laravel 13 the Eloquent `HasUuids` trait itself generates keys with `uuid7()`.

code

php · 11 lines
php
<?php

use Illuminate\Support\Str;

$random  = (string) Str::uuid();   // e.g. 9b2f0c1e-7c4d-4a9e-8f1b-...  (v4)
$ordered = (string) Str::uuid7();  // e.g. 0192b6e4-5a10-7c3e-9d2a-...  (v7, time first)
$ulid    = (string) Str::ulid();   // e.g. 01J9ZB6X3K8Q2R7M4T5V6W7X8Y   (26 chars)

Str::isUuid($ordered);             // true
Str::isUlid($ulid);                // true
Str::isUlid(Str::ulid());          // false: not a string

go deeper

for a junior

Know that Str::uuid() is random, Str::ulid() and Str::uuid7() are time-ordered, and that all of them must be cast to strings.

for a middle

Explain v4 versus v7 versus ULID formats, why time-first IDs index better, and how isUuid() and isUlid() treat non-strings.

for a senior

Choose identifier types per use — keys, public references, secrets — weighing index behaviour against timing leaks.

for a principal

Set an organisation-wide identifier policy covering key types, public exposure and migration away from random keys on large tables.

## The four generators `Illuminate\Support\Str` wraps two libraries — `ramsey/uuid` for UUIDs and `symfony/uid` for ULIDs: | Method | Returns | Format | Ordered by time? | |---|---|---|---| | `Str::uuid()` | `Ramsey\Uuid\UuidInterface` (v4) | 36 chars, hex with dashes | no, random | | `Str::uuid7($time = null)` | `Ramsey\Uuid\UuidInterface` (v7) | 36 chars | yes, millisecond timestamp first | | `Str::orderedUuid()` | `Ramsey\Uuid\UuidInterface` | 36 chars | yes, timestamp-first "COMB" layout | | `Str::ulid($time = null)` | `Symfony\Component\Uid\Ulid` | 26 chars, Crockford Base32 | yes, timestamp first | The optional `$time` argument lets you generate an ID for a specific moment, for example when back-filling records. ## They are objects None of these returns a string: ```php <?php use Illuminate\Support\Str; $id = Str::ulid(); $id === '01J...'; // false: object vs string $order->reference = (string) $id; // explicit and safe echo "Order {$id}"; // interpolation calls __toString() ``` Casting with `(string)` or calling `->toString()` on the Ramsey object is the habit to show in an interview. Validation helpers work on strings: `Str::isUuid($value)` (optionally with a version) and `Str::isUlid($value)` return `false` for non-strings. ## Why time ordering matters for keys Database indexes on primary keys are ordered structures. Random v4 values arrive in random order, so each insert lands somewhere in the middle of the index, touching pages all over it. Time-first IDs (v7, ULID, ordered UUIDs) arrive in roughly ascending order, so inserts append near the end, much like an auto-increment integer. The Laravel docs make the same point: time-ordered UUIDs are more efficient for indexed storage because they sort lexicographically. Side benefits: - `ORDER BY id` approximates creation order; - IDs can be generated in PHP before the insert, so you can reference a new order in a queued job or an event without a round trip. ## Choosing one 1. **Primary keys and order references:** `uuid7()` if the column is a native UUID type or you want the standard format; `ulid()` if you prefer a shorter, case-insensitive, URL-friendly string. 2. **Values that must not reveal timing:** `uuid()`. A v7 UUID or a ULID encodes its creation time in the first characters, so anyone holding the ID can read when it was made. 3. **Secrets:** none of them. Share tokens, password-reset links and API keys should come from a random generator such as `Str::random()` and be stored hashed, not from an identifier generator. 4. **Legacy compatibility:** `orderedUuid()` exists for older layouts; new code has little reason to choose it over `uuid7()`. ## How this connects to Eloquent In Laravel 13, the `HasUuids` model trait generates keys with `Str::uuid7()`, and a separate `HasVersion4Uuids` trait keeps the older generator; `HasUlids` uses `Str::ulid()`. The model-side configuration — which trait, which columns, the key type — is an Eloquent topic, but it helps to know that the framework's own default is a time-ordered UUID. ## Storing them The column type follows the generator: | Generator | Typical column | |---|---| | `uuid()` / `uuid7()` / `orderedUuid()` | a native UUID column where the database has one, otherwise a 36-character string | | `ulid()` | a 26-character string column | Laravel's schema builder offers `uuid()` and `ulid()` column methods for this; the migration details belong with the database layer. The point for this question is that **format and length differ**, so switching generators later means a data migration, not just a code change. ## Displaying them to customers Order numbers shown to customers are often a separate, friendlier value (a short sequence or a formatted code), with the UUID or ULID kept as the internal key. If you do show a ULID, its Base32 alphabet avoids easily confused characters such as `I`, `L`, `O` and `U`, which makes it easier to read over the phone than a hex UUID. ## Common mistakes - storing `Str::uuid()` into a model attribute and comparing it with `===` later; - exposing a ULID as a "secret" invoice link; - generating a random v4 UUID for a high-insert-rate primary key and then wondering why inserts slow down as the table grows.

  • Why is a ULID or UUIDv7 a poor choice for a password-reset or share link in Laravel?
    Its first characters are a timestamp, so part of the value is predictable and it leaks when the link was created. Identifier generators aim for uniqueness, not secrecy. Use a random token such as `Str::random(64)` and store only its hash.
  • What does Str::isUuid() return when given the result of Str::uuid() directly in Laravel?
    `false`. `Str::isUuid()` returns `false` for anything that is not a string, and `Str::uuid()` returns a `Ramsey\Uuid\UuidInterface` object. Cast first: `Str::isUuid((string) Str::uuid())` is `true`.

saying these in an interview costs you the question

  • Thinks Str::uuid() returns a plain string.
  • Uses random v4 UUIDs as high-insert primary keys without considering index order.
  • Treats a ULID as an unguessable secret token.
  • Believes a ULID and a UUID have the same length and format.
  • Claims time-ordered IDs reveal nothing about when a record was created.