Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cuid2 Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer and confirm your PHP version first — v7 requires PHP 8.3+ and v6 requires PHP 8.2+:

composer require visus/cuid2
# verify GMP is available (strongly recommended)
php -m | grep -i gmp

Read the README's Architecture section first (it explains what goes into the hash), then the Cuid2 class itself — it is a tiny API surface: new Cuid2($length), Cuid2::generate($length), ->toString(), ->__toString(), and Cuid2::isValid($value, expectedLength: n).

Smoke-test in Tinker before wiring anything up:

Cuid2::generate()->toString();       // "p6p168tx2rxtgyehd3p2wz04"
Cuid2::generate(10)->toString();     // "a1ao2r0lve"
Cuid2::isValid('a1ao2r0lve', expectedLength: 10); // true

First use case: replace auto-incrementing keys on a model whose IDs are exposed in URLs (posts, invoices, upload tokens). CUID2 is URL-safe base36 ([a-z][a-z0-9]{3,31}), so it drops straight into route model binding with no encoding concerns.

// migration
$table->string('id', 24)->primary();

Implementation Patterns

Model keys — use Laravel's built-in trait, not a custom booted() hook.

use Illuminate\Database\Eloquent\Concerns\HasUniqueStringIds;
use Visus\Cuid2\Cuid2;

class Post extends Model
{
    use HasUniqueStringIds;

    public function uniqueIds(): array
    {
        return ['id'];
    }

    public function newUniqueId(): string
    {
        return Cuid2::generate()->toString();
    }
}

HasUniqueStringIds already handles $incrementing = false and $keyType = 'string', and it fires on creating, so the ID is never regenerated on update — safer than writing your own booted()/saving() listener.

Foreign keys must match the column type. foreignIdFor(Post::class) will emit a bigint — use the string form instead:

$table->string('post_id', 24);
$table->foreign('post_id')->references('id')->on('posts')->cascadeOnDelete();

Route binding needs no work, but you can fail fast on malformed segments instead of hitting the DB:

Route::get('/posts/{post}', ShowPost::class)
    ->where('post', '[a-z][a-z0-9]{23}');

Validation. isValid() makes a clean custom rule; use expectedLength so a 10-char public ID can't be passed where a 24-char key is expected.

class Cuid2Rule implements ValidationRule
{
    public function __construct(private int $length = 24) {}

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (! is_string($value) || ! Cuid2::isValid($value, expectedLength: $this->length)) {
            $fail("The {$attribute} is not a valid identifier.");
        }
    }
}

Factories and seeders — always call ->toString(). Factories re-evaluate closures per model, so this gives a fresh ID each time:

public function definition(): array
{
    return ['id' => Cuid2::generate()->toString()];
}

Non-key uses that pay off daily: idempotency keys for queued jobs, batch_id on uploads, filenames for generated PDFs/exports, and opaque pagination cursors. A Str macro keeps call sites tidy:

Str::macro('cuid2', fn (int $length = 24): string => Cuid2::generate($length)->toString());

Length policy. Use 24 (the default) for primary keys; 10–16 for user-facing tokens where the value is short-lived or rate-limited. 4 is technically valid and hopelessly collision-prone at any real volume.

Isolate it behind one app-level class (app/Support/Id.php) with Id::new() / Id::isValid(). With 44 stars and zero dependents, a one-file seam lets you swap in Symfony\Uid\Ulid or Str::ulid() later without a codebase-wide sed.

Gotchas and Tips

  • isValid() is a format check only. It does not prove the value was generated by this library, nor that it is unique. Never use it as an authorization or "record exists" check — always query the DB.
  • Never pass a Cuid2 object to Eloquent/PDO/json_encode. The class is Stringable, but json_encode($cuid) on a non-JsonSerializable object serializes public properties and yields {}, and PDO binding of objects is version-dependent. Cast ->toString() at the persistence and serialization boundary; keep the object only for local echo.
  • Install GMP in production. The fallback pure-PHP base36 conversion is ~60–300× slower (≈248 µs vs 0.81 µs for a SHA3-512 digest). That's invisible for one model save per request, and brutal in a 100k-row import or a factory()->count(50000) seeder — minutes instead of seconds. Add RUN docker-php-ext-install gmp (or install-php-extensions gmp) to your image and ext-gmp to your CI matrix.
  • PHP floor drives your version pin. Laravel 11/12 on PHP 8.2 → pin ^6; on PHP 8.3+ → ^7. composer require will resolve correctly, but composer.json in a legacy app can silently hold you back.
  • v6+ generates the ID in the constructor, so new Cuid2() then repeated ->toString() is cheap — build once, read many. Do not instantiate in a loop "to be safe"; that costs a full SHA3-512 + base conversion each time.
  • Don't trust the README's "sortability" claim in practice. The timestamp is one input to a SHA3-512 hash that is then truncated, so lexicographic order does not track creation order. Order by created_at (indexed) or use an ordered ULID/UUIDv7 if sortable keys are a hard requirement.
  • There is no config file, facade, or service provider. Nothing to publish; it's a plain library. That's a feature — but it also means no container binding, so mock it via your own Id wrapper in tests.
  • No configurable alphabet or prefix control. If you need prefixed, Stripe-style IDs (post_abc123), wrap the output yourself and account for the extra length in your column definition.
  • Fingerprint inputs include hostname, PID, and environment data (since 5.1.0). That's fine for uniqueness, but it means IDs differ across your local and CI environments — never assert exact ID values in tests. Assert shape instead: expect($id)->toMatch('/^[a-z][a-z0-9]{23}$/') or assertTrue(Cuid2::isValid($id, 24)).
  • Keep the unique index on the column. Collision resistance is probabilistic; a DB constraint is the actual guarantee, and it also catches the case where someone hand-assigns an ID in a seeder.
  • Debugging trick: if generation throws, it's almost always missing sha3-512 support (checked in the Cuid2 class) or a length outside 4–32 — not a randomness problem. Verify with in_array('sha3-512', hash_algos(), true).
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel