matt-daneshvar/eloquent-hashids
Laravel package that adds Hashids support to Eloquent models, hiding auto-increment IDs behind short, reversible hashes. A trait encodes route keys and resolves models from hashids, keeping URLs clean and database IDs private with minimal setup.
This is a small, single-purpose package: it generates a Hashid from a model’s primary key and persists it when the model is created. Treat it as legacy infrastructure — last release 2019 and archived — but it can still be useful in an older Laravel app.
composer require matt-daneshvar/eloquent-hashids
hashid string column, indexed/unique:Schema::table('posts', function (Blueprint $table) {
$table->string('hashid')->nullable()->unique()->after('id');
});
php artisan vendor:publish --provider="MattDaneshvar\EloquentHashids\ServiceProvider"
php artisan config:clear
use MattDaneshvar\EloquentHashids\HasHashids;
class Post extends Model
{
use HasHashids;
}
hashid instead of the numeric ID in an API response or route.$post = Post::create(['title' => 'Hello']);
// $post->hashid should now be populated
return response()->json([
'id' => $post->hashid,
'title' => $post->title,
]);
Where to look first:
creating.composer.json constraints before installing into Laravel 9/10/11 — this package predates them.Route model binding by hashid
Override the route key and resolve by the hashid column:
class Post extends Model
{
use HasHashids;
public function getRouteKeyName(): string
{
return 'hashid';
}
}
Then routes can use /posts/{post} and Laravel will resolve via hashid:
Route::get('/posts/{post}', [PostController::class, 'show']);
If you need explicit control:
Route::get('/posts/{hashid}', function (string $hashid) {
return Post::where('hashid', $hashid)->firstOrFail();
});
API resources
Keep numeric IDs internal and expose hashids consistently:
class PostResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->hashid,
'title' => $this->title,
];
}
}
Mixed internal/external IDs
Use numeric IDs for joins, foreign keys, and internal queries. Use hashids only at the boundary: URLs, public API payloads, webhook references.
$post = Post::findOrFail($internalId);
return new PostResource($post); // exposes $post->hashid
Backfilling existing records
The trait normally generates on model creation only. Existing rows will not have hashids. Backfill with a queued command so you don’t block a deployment:
Post::whereNull('hashid')->chunkById(500, function ($posts) {
foreach ($posts as $post) {
$post->hashid = app(Hashids::class)->encode($post->getKey());
$post->saveQuietly();
}
});
Verify the package’s API for generating a hashid directly; otherwise use the underlying hashids/hashids instance with the same salt and length.
Lookups from public input
Add a small helper or query scope to avoid repeating where('hashid', ...):
public function scopeWhereHashid($query, string $hashid)
{
return $query->where('hashid', $hashid);
}
Relationships and nested routes
Resolve the parent by hashid, then use normal numeric foreign keys for children:
Route::get('/posts/{post}/comments/{comment}', function (Post $post, Comment $comment) {
abort_unless($comment->post_id === $post->id, 404);
return $comment;
});
Testing
In tests, assert against generated values rather than hardcoding them unless you deliberately pin the salt in the test environment:
$post = Post::factory()->create();
$this->assertNotEmpty($post->hashid);
$this->getJson("/api/posts/{$post->hashid}")->assertOk();
Seeding and imports
Eloquent events fire on normal save() and create(), so seeders using Eloquent should get hashids. Bulk insert() calls bypass model events and will not. If you need deterministic hashes across environments, set the same salt and min length in each environment’s config.
creating listener.Model::insert(), raw SQL imports, and DB::table()->insert() will not trigger hashid generation. Use Eloquent saves or backfill explicitly.saveQuietly(), withoutEvents(), and Model::withoutEvents() prevent generation. If you use them during imports, set hashids manually.php artisan config:clear and redeploy. Cached config is a common source of “why did the hash change?” confusion.hashid, those records become unreachable until backfilled.$fillable. But if you’re manually assigning hashid in a controller, ensure it is guarded or removed from request input to prevent clients from choosing their own public identifier.hashids/hashids library available so you can decode old values during migration.How can I help you explore Laravel packages today?