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

Eloquent Hashids Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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.

  1. Install it and confirm it supports your Laravel/PHP versions:
composer require matt-daneshvar/eloquent-hashids
  1. Add a column for the hashid. Default assumptions are usually a hashid string column, indexed/unique:
Schema::table('posts', function (Blueprint $table) {
    $table->string('hashid')->nullable()->unique()->after('id');
});
  1. Publish the config if the package exposes it, then inspect the salt, minimum length, alphabet, and column name:
php artisan vendor:publish --provider="MattDaneshvar\EloquentHashids\ServiceProvider"
php artisan config:clear
  1. Add the package trait to each model that should receive a hashid:
use MattDaneshvar\EloquentHashids\HasHashids;

class Post extends Model
{
    use HasHashids;
}
  1. First use case: create a model and expose 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:

  • The published config file for salt/length/column settings.
  • The trait source to confirm the exact column name and whether generation is hooked into creating.
  • composer.json constraints before installing into Laravel 9/10/11 — this package predates them.

Implementation Patterns

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.

Gotchas and Tips

  • Archived and old. Last release was June 2019. Check PHP and Laravel compatibility carefully. On modern Laravel, prefer a maintained alternative or implement the small amount of logic yourself via a model creating listener.
  • Create-only by design. The trait typically hooks model creation. Updates, restores, imports, and manual SQL will not regenerate hashids. If your primary key can ever change, the hashid will become stale.
  • Bulk operations bypass events. Model::insert(), raw SQL imports, and DB::table()->insert() will not trigger hashid generation. Use Eloquent saves or backfill explicitly.
  • Quiet saves skip events. saveQuietly(), withoutEvents(), and Model::withoutEvents() prevent generation. If you use them during imports, set hashids manually.
  • Hashids are obfuscation, not encryption. Anyone with the salt can decode them. Do not use them as secrets, signed tokens, or authorization checks. Keep salt values in environment/config, not in source control.
  • Changing the salt or minimum length invalidates existing values. Old hashids will no longer decode with the new configuration, and lookups will fail. Treat salt changes as a data migration.
  • Collisions are unlikely but not impossible. Add a unique index on the hashid column and be prepared to retry generation or handle duplicate-key exceptions during bulk backfills.
  • Column length and nullability. Hashids with a larger minimum length or alphabet can exceed the default assumption. Size the column generously, especially before backfilling. Existing rows may need a nullable column until the backfill completes.
  • Config caching. After changing salt, length, alphabet, or column name, run php artisan config:clear and redeploy. Cached config is a common source of “why did the hash change?” confusion.
  • Route binding returns 404 for null hashids. If existing records have no hashid and you bind by hashid, those records become unreachable until backfilled.
  • Mass assignment is not usually the issue. Traits that set attributes directly bypass $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.
  • Extension points. Inspect the trait and config for overridable methods or per-model settings. If the package does not support per-model salts/columns, wrap generation in your own trait or model observer instead of fighting the package.
  • Upgrade path. Because the package is archived, plan an exit: either inline the generation logic into a local trait/observer or switch to a maintained Hashids package. Keep the underlying hashids/hashids library available so you can decode old values during migration.
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