visus/cuid2
A PHP implementation of CUID2 for generating secure, URL-safe, collision-resistant identifiers across multiple machines without coordination. Includes instance-based and static APIs plus built-in validation.
Architecture fit. CUID2 is a pure value-generation library: three classes (Cuid2, Fingerprint, Utils), no service provider, no config file, no facade, no framework coupling. That is a strength for us — it drops into any PHP 8.3 codebase and imposes no container, HTTP, or storage opinions. The design is deliberately stateless at the process level (timestamp + monotonic counter + host/PID/environment fingerprint + CSPRNG bytes → SHA3-512 → base36), so horizontally scaled app servers can mint IDs without coordination, a requirement for our multi-node/queue-worker topology. Two behaviors are architecturally material: (1) since v6 the identifier is generated in the constructor and cached, so a Cuid2 instance is a single immutable ID, not a factory — resolving it as a container singleton would return the same ID forever; use Cuid2::generate() or construct per-ID; (2) the class deliberately blocks __clone/__wakeup, so the object cannot be serialized into queue payloads or the cache — only (string) casts may cross boundaries. Neither is a blocker; both must be encoded in our wrapper or they will be violated in review.
Integration feasibility. High. Composer install, MIT licensed, no transitive dependencies since 6.0.0 (the markrogoyski/math-php and symfony/polyfill-php83 deps were removed), GMP optional with an identical-output pure-PHP fallback. The work is not adoption of a package, it is writing the ~100 lines of Laravel glue that the package intentionally omits: an IdGenerator contract, a Cuid2 adapter, a provider binding, and an Eloquent trait/model convention ($incrementing = false, $keyType = 'string'). That is roughly 1–2 engineer-days plus tests and is largely copy-adaptable from existing ULID/UUID trait patterns.
Technical risk.
unique external-ID column.isValid() is format-only. It must never be used as an authenticity or authorization check.Key questions. Is PHP 8.3 the fleet baseline (i.e., are we on Laravel 11/12-era runtimes)? Is ext-gmp present in every prod image, and do we care enough to enforce it? Do we need time-ordered IDs (→ ULID/UUIDv7) or merely opaque, non-enumerable ones (→ CUID2)? Is CUID2 a primary key or an external identifier? Do any non-PHP services need to generate or validate these values, and has cross-language format parity been verified against the reference implementation? Who owns the wrapper and the length constant? (Also flag: the metadata's last-release date of 2026-09-21 is future-dated — verify the actual tag on Packagist before pinning.)
Stack fit. Good for Laravel 11/12 on PHP 8.3. No framework conflict: it does not overlap with symfony/uid, ramsey/uuid, or Laravel's built-in HasUuids/HasUlids traits, so it can coexist per-model. Its output is lowercase base36, URL-safe, and case-insensitive-collation-safe — no escaping or encoding concerns in routes, JSON, or S3 keys.
Migration path. Do not rewrite existing primary keys. Introduce CUID2 additively: new tables get a cuid column (or an external-ID pattern beside the existing auto-increment PK); existing tables get a nullable column, backfilled in batches, then a unique index. Dual-write during transition, with reads keyed on the legacy column until the new column is populated everywhere. Only if a table is small and low-traffic should PK replacement even be considered; the index-rebuild and FK-cascade cost dominates otherwise. Rollback is clean because every step is additive — stop generating and drop the column; nothing depends on it until the cutover is complete.
Compatibility. Lock one length (recommend 24, the default) in a single application constant and reuse it for isValid($value, expectedLength: X); changing the length later invalidates stored IDs against validation. Avoid lengths below ~10 for anything externally exposed — the 4-character minimum is far too little entropy. Enforce (string) casting at every boundary (queue payloads, cache, logs, session) so the __wakeup/__clone guards are never tripped. If Octane/RoadRunner is in play, confirm nothing holds a Cuid2 instance as a singleton.
Sequencing.
App\Contracts\IdGenerator, Cuid2Generator adapter, provider binding, HasCuid trait, php artisan id:healthcheck that generates one ID and asserts format/length at deploy time.new Cuid2() outside the adapter.Maintenance. Low code surface, zero runtime dependencies, MIT. The real maintenance load is version drift: three majors in recent history raised the PHP floor or flipped GMP from optional to required to optional again. Pin a minor (composer require visus/cuid2:^7.0 with a committed lockfile), gate upgrades behind a regression test that asserts output shape and length, and keep the adapter thin enough that replacing the library with symfony/uid is a one-file change. Given 0 dependents and a single maintainer, treat this as a vendorable dependency: the source is small enough to fork or pin indefinitely if upstream stalls.
Support. No commercial support; community-only. Internal support burden concentrates in the adapter owner. Two things to publish internally: (a) CUID2 is not time-sortable despite the README's "sortability" bullet — use created_at for ordering; (b) isValid() is format-checking only and must not gate access to a record. Both are cheap doc fixes that prevent expensive misuse.
Scaling. Generation scales horizontally with no coordination — the fingerprint plus per-process counter handles concurrency within a node, and the CSPRNG handles the rest. Throughput is bounded by hashing plus base conversion, i.e., fast with GMP and roughly 4k IDs/sec/node without it. That is fine for request-path generation and marginal for bulk imports or fixtures — plan to enable GMP in CI and prod, and benchmark before any large seed/backfill job. The scaling pressure point is not the generator, it is the database: random, non-ordered keys on a clustered index cause fragmentation at high insert rates. The external-ID-column pattern sidesteps this entirely and should be the default recommendation.
Failure modes and mitigations.
extension_loaded('gmp') in the deploy health check.__wakeup/__clone guards fire if the object is queued or cached. Mitigate: adapter returns string, never the object; enforce with a return type.Cuid2 from the container yields one ID reused for every model. Mitigate: bind the contract to a stateless factory, not the class.isValid() against stored IDs. Mitigate: version the constant or validate without expectedLength on read paths.Ramp-up. Effectively zero for a mid-level Laravel developer: two constructor forms, one static, one validator. Budget one short internal note covering the four gotchas (not time-sortable, format-only validation, object-vs-string, length is a schema decision) and the adapter enforces the rest structurally. The heavier lift is the organizational conversation about identifier strategy — PK vs external ID, and CUID2 vs ULID/UUIDv7 — which should happen once, up front, not per team.
How can I help you explore Laravel packages today?