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

Laravel Ulid Laravel Package

rorecek/laravel-ulid

rorecek/laravel-ulid adds ULID support to Laravel apps, making it easy to generate lexicographically sortable, URL-safe identifiers and use them with Eloquent models. A lightweight alternative to UUIDs for primary keys and other unique IDs.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require rorecek/laravel-ulid

The service provider is auto-discovered; there is nothing to register in config/app.php on Laravel 5.5+. If you want to inspect what the package actually does, look at the trait first — the entire package surface is a trait that flips three Eloquent switches ($incrementing = false, $keyType = 'string', and generating a ULID on creating). There is no meaningful runtime config to learn.

Wire up your first model

use Rorecek\Ulid\HasUlid;

class Post extends Model
{
    use HasUlid;
}

Then make the column match in a migration:

Schema::create('posts', function (Blueprint $table) {
    $table->ulid('id')->primary();   // or $table->char('id', 26)->primary();
    $table->string('title');
    $table->timestamps();
});

First use case to reach for: new tables where you want a non-enumerable, sortable public identifier — e.g. Post, Order, Invoice. Create a record and confirm $post->id is a 26-character Crockford base32 string and that Post::find($post->id) and route model binding (/posts/{post}) both work without changes.

If you are on Laravel 9+, stop here and compare against Illuminate\Database\Eloquent\Concerns\HasUlids — see Gotchas.

Implementation Patterns

Apply the trait at the base-model level. Rather than sprinkling use HasUlid; on forty models, drop it on your abstract BaseModel (or a HasPublicId concern) so every child inherits the behaviour, and opt out for the few models that genuinely need auto-increment joins.

Keep the column type consistent end-to-end. A ULID primary key is only useful if the foreign keys pointing at it are the same type. Use $table->foreignUlid('post_id') (Laravel 8.30+) or $table->ulid(...) on both sides; mixing char(26) and varchar(26) between tables will silently degrade index performance and can trip MySQL collation comparisons.

Order by id instead of created_at. ULIDs are lexicographically sortable by generation time, so Post::orderBy('id')->get() is a cheap, index-backed chronological listing — no timestamp column needed for the cursor. This is the single biggest day-to-day payoff, and it composes well with cursor pagination (cursorPaginate() on id).

Generate IDs for records you don't save through the model. For raw DB::table()->insert(), queue payloads, or child rows created before the parent exists, expose a small helper that wraps the package's generator (or Str::ulid() on Laravel 9+) and use it everywhere instead of calling uniqid() or Str::uuid().

Validate at the edge. Route model binding handles bad IDs by 404-ing, but for request payloads (e.g. post_id in a nested create) validate with Str::isUlid($value) on Laravel 9+ or a regex for [0-9A-HJKMNP-TV-Z]{26}. Reject before the query layer.

Factories and seeds. Because the trait fills the key on creating, factory-created models get IDs automatically. If you need deterministic IDs in tests, set id explicitly in the factory state — the trait should respect an already-set key (verify this; if it overwrites, add a creating guard in your own base model that only fills when $model->getKey() === null).

API resources. Treat the ID as a string in transformers — never run it through (int). ULIDs make good public identifiers precisely because they leak nothing about row count, so also avoid exposing an internal auto-increment id alongside them.

Gotchas and Tips

You probably don't need this package on Laravel 9+. The framework ships HasUlids and Str::ulid(), plus $table->ulid() and foreignUlid() in the schema builder. The framework version is maintained against current releases and interoperates with newUniqueId(). Adopt this package only on Laravel 8 or earlier, or if you're pinned to its specific behaviour. Plan the migration: swap the trait import and confirm the package doesn't also override newUniqueId() in a way your code calls directly.

Column type must be exactly sized. $table->string('id') (varchar 255) works but wastes index space and defeats the compact-ULID benefit; char(26) or the native ulid() type is correct. On MySQL, don't put a case-insensitive-friendly utf8mb4 collation on the column — ULIDs are Crockford base32 and case handling on lookup should be deliberate. Store uppercase (the common convention) and normalise input.

Route model binding works, but only if getRouteKeyName() isn't overridden. If a model previously used a slug, adding HasUlid won't change bindings; keep the slug binding and use the ULID for internal joins, or explicitly set getRouteKeyName() => 'id'.

Ordering is time-based, not strictly monotonic. Two ULIDs generated in the same millisecond sort randomly relative to each other (the random component is 80 bits). Within a single process using the common ulid library, monotonic mode may be available — don't rely on insert order matching ID order across concurrent workers, and don't use "highest ID" as a concurrency-safe cursor without a tiebreaker.

Trait boot-order conflicts. The trait hooks creating via a bootHasUlid() method. If you also implement your own booted()/creating listener that sets the key — common when backfilling legacy rows — the last writer wins. Debug by dd()-ing inside creating; if the ID is null at insert time, the trait isn't being booted (wrong namespace, trait on a parent that isn't actually extended, or a model using $guarded = [] with an explicit id that shadows it).

Debugging checklist for a "null id" 500: $model->getIncrementing() should be false, $model->getKeyType() should be 'string', and $model->getKeyName() should be 'id'. If getIncrementing() is still true, the trait isn't applied. If it's false but inserts fail, the column is likely int/bigint.

Backfilling existing tables. You cannot just change the column type. Create a new ulid column, backfill with generated ULIDs (batched, ordered by the old id so chronology is preserved), then swap foreign keys and promote the column to primary. Do this in a maintenance window or with a shadow-write strategy; it is not a zero-downtime migration with this package alone.

Extension points. ULID generation happens in one place in the trait; override that method in a BaseModel if you need a specific monotonic generator, a prefix scheme, or binary (BLOB) storage instead of a 26-char string. For binary storage you lose human readability and MySQL's ulid() type, so treat that as a deliberate performance decision rather than a default.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin