Download the PHP package intrfce/laravel-public-ids without Composer
On this page you can find all versions of the php package intrfce/laravel-public-ids. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download intrfce/laravel-public-ids
More information about intrfce/laravel-public-ids
Files in intrfce/laravel-public-ids
Package laravel-public-ids
Short Description Stripe-style, reversible prefixed public IDs for Eloquent models, backed by UUID v7.
License MIT
Informations about the package laravel-public-ids
Note: This is not production ready, and the docs aren't ready yet.
Laravel Public IDs
Stripe-style public identifiers for Eloquent models — user_3kQ4mZp8Vh7kQp2Rt5Nx9, cus_0192f8a1... — that are a reversible base62 encoding of the model's UUID v7 primary key.
Unlike a real Stripe ID (an opaque random token), this ID decodes back to the UUID with pure math — no database lookup. That makes it fast and stateless, at the cost of not being opaque (see Caveats).
Requirements
- PHP 8.3+
- Laravel 12+
Installation
The service provider is auto-discovered.
Setup
1. Add the trait and declare a prefix
Each model owns its prefix, returned from an idPrefix() method on the class — there is no central registry to keep in sync:
The trait builds on Laravel's own HasUuids, so the key is a non-incrementing string auto-populated with a UUID v7 on create (override newUniqueId() to change that). idPrefix() is abstract — a model that uses the trait without implementing it won't compile, so there is no runtime "missing prefix" state to guard against.
2. Use a UUID primary key in your migration
Usage
Reading the ID
The raw UUID stays the ORM key — relationships, eager loading, and foreign keys are never touched, so everything keeps working normally.
JSON / API output
toArray() and toJson() present the Public ID as id and hide the raw UUID:
Route model binding
URLs use the Public ID automatically, and implicit binding decodes it:
Querying
find(), findOrFail(), findMany(), destroy(), and whereIn('id', …) all accept either a bare UUID or a Public ID:
Assigning the key
Note the deliberate asymmetry: you may assign a Public ID, but reading
$user->idreturns the raw UUID. Read$user->public_idfor the prefixed form.
Validation
Laravel's built-in exists rule runs a raw query and cannot match a Public ID against a UUID column (it silently fails on MySQL and 500s on Postgres). Use the decode-aware rule instead, naming the model directly:
Bad input (malformed tail, wrong prefix) fails as a normal validation message — it never throws. The target model must use HasPublicIds; its prefix is read from idPrefix().
Exceptions
All extend Intrfce\PublicIds\Exceptions\PublicIdException:
| Exception | When |
|---|---|
PrefixMismatchException |
assigning/querying with another model's prefix |
InvalidPublicIdException |
malformed value or a tail that isn't valid base62 |
How it works
"The id" is two things that this package deliberately keeps separate:
- ORM key — the raw UUID v7. What
getKey(), foreign keys, joins, and eager loading use. Never changed. - Public ID —
<prefix>_<base62(uuid bytes)>, ~22-char tail, URL-safe. Shown on JSON, URLs, and thepublic_idaccessor.
Decoding is pure integer arithmetic over the UUID's 16 raw bytes (no ext-gmp/ext-bcmath required). The full rationale for every decision lives in docs/ as ADRs.
Performance
A fair question is whether encoding and decoding every ID becomes a bottleneck at scale. In practice it doesn't, and the design is the reason why.
Both operations are pure in-memory integer arithmetic over a UUID's 16 raw bytes — no database round-trip, no I/O, no extension. Each is O(1): a fixed 128-bit number, always a ~22-character base62 tail. The informational benchmark (ADR-0009) times 1000 of each:
That's on the order of ~0.01 ms per ID — roughly json_encode-ing a small array, and orders of magnitude below a single database query. Serializing a 100-row API page adds around 1 ms of codec time. (Numbers are hardware-dependent; run the benchmark on your own target.)
Two design choices keep this off your hot paths:
- The codec is reversible with math, so there is no lookup. Opaque-token schemes (a random public column you query by) need an extra indexed column and a database hit to translate every incoming ID. Here the translation is a local computation, so it scales with CPU rather than with database load.
- The raw UUID stays the ORM key, so the codec never touches your queries. Joins, foreign keys, eager loading, and any internal
getKey()-driven query use the native UUID and skip encode/decode entirely — bare UUIDs pass straight through the query builder (ADR-0004 / 0014). You pay the codec cost only when an ID crosses the boundary: encoding on JSON/URL output, decoding on route binding or afind()handed a Public ID.
There is no automatic regression gate — the benchmark reports rather than asserts (ADR-0009) so it never flakes across CI runners. If drift ever matters to you, that benchmark is the hook to add an env-gated threshold.
Caveats
- Not opaque. A Public ID reveals its UUID, and because UUID v7 embeds a creation timestamp, it reveals when the record was created. If you need unguessable external IDs, this is the wrong tool.
- A prefix is mandatory. A model using the trait must implement
idPrefix()— the method is abstract, so this is enforced at compile time, not at runtime. - No global resolver. Because prefixes live on models (not in a central map), there is no
resolve('cus_…') -> Customerlookup from a bare Public ID. Operations are model-scoped:Customer::find('cus_…'), route binding,$model->public_id. - set/get asymmetry on the key is intentional — assign a Public ID, read back a raw UUID.
Testing
The suite includes an informational benchmark (never gates the build); see Performance for what the numbers mean:
License
MIT.
All versions of laravel-public-ids with dependencies
illuminate/support Version ^12.0 || ^13.0
illuminate/database Version ^12.0 || ^13.0
illuminate/contracts Version ^12.0 || ^13.0