phpexperts/conciseuuid
ConciseUUID is a PHP package for generating compact, URL-safe UUIDs. It encodes standard UUIDs as shorter Base62 strings, cutting identifier length for URLs, databases, and APIs while preserving uniqueness. Simple API works in Laravel or any PHP project.
Install the package:
composer require phpexperts/conciseuuid
Generate a migration that uses a string primary key sized for base62 UUIDs (22 chars), not the default bigIncrements:
Schema::create('orders', function (Blueprint $table) {
$table->char('id', 22)->primary();
$table->string('customer_name');
$table->timestamps();
});
Have your model use the package's base model (or trait — check vendor/phpexperts/conciseuuid/src for the exact class name and whether it ships a ConciseUuidModel base class or a trait):
use PHPExperts\ConciseUuid\ConciseUuidModel;
class Order extends ConciseUuidModel
{
protected $guarded = [];
}
The base model is responsible for setting $incrementing = false, $keyType = 'string', and generating the base62 UUID in a creating hook.
First use case: models that end up in URLs or public APIs where a 36-char UUID is noisy. Create a record, confirm $order->id is a 22-char string, then wire route model binding:
Route::get('/orders/{order}', [OrderController::class, 'show']);
Because the key is a string, Laravel's implicit binding works without extra configuration as long as the controller method type-hints Order.
Where to look first: the package is small. Read the single model/trait source file before using it — it will tell you exactly which method generates the UUID and how the model boots, which is what you need for every pattern below.
Extend the base model for new tables. This is the least-surprising path: any model extending the package's base class inherits the non-incrementing string key and the creating hook that fills it.
Apply the trait to existing models. If you already extend a custom base model, use the package trait (if provided) and replicate the boot method:
protected static function bootConciseUuid(): void
{
static::creating(function ($model) {
if (empty($model->{$model->getKeyName()})) {
$model->{$model->getKeyName()} = /* package generator */;
}
});
}
The empty() guard matters — it lets you assign an explicit ID (seeders, imports, tests) without being overwritten.
Migrations that reference a concise-UUID table. Foreign keys must match the referenced column type:
$table->char('order_id', 22);
$table->foreign('order_id')->references('id')->on('orders');
Using $table->foreignId('order_id') will fail — it creates a bigint unsigned, which won't match a char(22) column.
Relationships. Use belongsTo/hasMany normally. The relationship resolves fine because Eloquent compares string keys with WHERE id = ?; just make sure the owning model's $keyType is 'string' so Eloquent doesn't cast the key to int during hydration (the base model handles this if you extend it).
API resources and URLs. The main payoff is shorter, non-sequential identifiers. When exposing IDs in JSON, keep them as strings and never (int) cast them:
return [
'id' => $this->id, // "3kJ9aZ..."
'created_at' => $this->created_at->toIso8601String(),
];
Also consider getRouteKeyName() (returns the primary key by default, so usually nothing to do) and resolveRouteBinding() only if you need custom lookup logic.
Factories and seeders. Since IDs are strings, don't assume integers. Factories should leave the id unset so the creating hook generates it, or explicitly set a deterministic value for fixture tests:
Order::factory()->create(['id' => '0000000000000000000000']);
Polymorphic relations work the same way, but confirm the *_id columns are char(22)/string, since the default morphs() helper creates bigint columns. You'll need to hand-write them:
$table->char('commentable_id', 22);
$table->string('commentable_type');
Bulk inserts. The creating hook only fires per-model. DB::table('orders')->insert([...]) and Model::insert() bypass it entirely — you must generate IDs yourself in bulk paths or switch to Model::create()/upsert.
char(22) primary key with a bigint foreign key produces confusing "Cannot add foreign key constraint" errors in MariaDB/MySQL. Always match the referenced column definition exactly, including length and collation.$incrementing and $keyType. If you don't use the package's base model, you must set public $incrementing = false; and protected $keyType = 'string'; yourself, or Eloquent will try INSERT ... id = 0 and cast keys to int on read, silently truncating leading characters.protected $guarded = []; or add id to $fillable only if you intend to allow client-supplied IDs. Otherwise keep id guarded so users can't choose keys.created_at index for range queries rather than relying on id order.string('id', 36) is a safe fallback if you're unsure.creating hook likely isn't booting — check the model extends the right base class, that the trait's boot{TraitName} convention matches, and that you haven't overridden boot() without calling parent::boot().RefreshDatabase, IDs are regenerated per test, so never assert on a specific ID value. Assert on format instead (e.g., length 22, matches /^[0-9A-Za-z]{22}$/).creating hook rather than editing the vendor file. The package is small enough that copying its single generator into your own trait is often the lower-risk move.composer.json, and be prepared to inline the (short) generator if the package stops receiving Laravel compatibility updates.How can I help you explore Laravel packages today?