naabster/laravel-eloquent-hashids
Integrate Hashids with Laravel Eloquent models: automatically encode numeric IDs into short, obfuscated strings and decode them back. Supports route model binding, custom salts, and seamless model lookups for cleaner, safer URLs.
This package is a thin Laravel integration around vinkla/hashids, giving you short, reversible, non-sequential identifiers for integers. Its job is to keep auto-increment primary keys out of URLs, API payloads and emails while remaining stateless (no lookup table, no extra column).
Minimal setup:
composer require <vendor>/<package> "^1.0"
php artisan vendor:publish --provider="Vinkla\Hashids\HashidsServiceProvider"
// .env — never commit this value, and never change it once links are in the wild
HASHIDS_SALT=some-long-random-string
Where to look first:
config/hashids.php — the default connection plus per-connection salt, length and alphabet.Hashids::connection('main') or inject the manager, since this determines how you test it.config/app.php (Laravel 5 packages of this vintage still expect manual provider + alias registration).First real use case — opaque IDs in routes:
Route::get('users/{user}', 'UserController@show');
// controller
public function show($user)
{
$id = Hashids::decode($user); // returns [] on garbage input
abort_if(empty($id), 404);
return User::findOrFail($id[0]);
}
If you only do one thing with this package, do that — and then immediately move to route-key binding below, because hand-rolling decode/abort in every controller does not scale.
Route model binding via the route key. The cleanest integration is to teach the model to speak hashids, so controllers and route() helpers stay untouched:
trait HasHashidRouteKey
{
public function getRouteKey()
{
return Hashids::encode($this->getKey());
}
public function resolveRouteBinding($value)
{
$decoded = Hashids::decode($value);
return $this->where($this->getKeyName(), $decoded[0] ?? null)->first();
}
}
On older 5.x point releases without resolveRouteBinding, override getRouteKeyName() is not enough — you need a Route::bind('user', ...) in RouteServiceProvider instead. Check which your Laravel minor supports before copying snippets.
API resources / presenters. Encode at the boundary, never in the model's toArray():
return [
'id' => Hashids::encode($this->id),
'name' => $this->name,
// relations referenced by id need encoding too
'team_id' => Hashids::encode($this->team_id),
];
Encoding in toArray() looks convenient until you need the raw integer internally and start calling getRawOriginal() everywhere. Keep DB-facing code numeric, presentation-facing code hashed.
Inbound validation. Decoding twice (once in a form request, once in the controller) is a common smell. Either add a rule:
Validator::extend('hashid', function ($attribute, $value) {
return count(Hashids::decode($value)) === 1;
});
…or resolve in a form request and pass the model forward via $request->route()->setParameter().
Multiple connections. Use separate connections, not a single global salt, when two entity types must not be mutually decodable — e.g. users and invoices connections. A hashid from one connection will not decode under another, which gives you a cheap cross-tenant / cross-entity guard.
Queues and exports. Anything that stores a URL (mailables, queued notifications, CSV exports, sitemaps) must encode at generation time, not render time. Persisted links outlive config changes.
.env, and rotate only with a migration plan — rotation instantly invalidates every link, bookmark, and third-party integration.decode() never throws. It returns [] for anything it cannot parse. Every call site needs an explicit empty check, or you will silently findOrFail(null) and produce confusing 404s.length is cosmetic, not security. A min length of 8 makes IDs look uniform; it does not add entropy. Only the salt and the input range do.Hashids::decode($x)[0] is a string; comparing it to an int with === will fail. Cast before use.WHERE hashid = ?. Always decode first, then query by primary key — otherwise you invite full table scans.phpunit.xml (<env name="HASHIDS_SALT" value="test-salt"/>) so fixtures are stable, and assert against encoded values only where the exact string matters.vinkla/hashids to the Laravel 5-compatible line — no API change on your side, but it does change the transitive dependency tree. Update with composer update vinkla/hashids <vendor>/<package> --with-dependencies, then run the suite: the Laravel 5 branch of vinkla/hashids changed its own service provider registration and config publishing semantics, so a stale config/hashids.php from an earlier install may not match the new key names. If php artisan vendor:publish was run under the old version, diff your published config against the new package default rather than assuming it still applies. Pin both packages in composer.json and avoid ^1.0 ranges if you deploy frequently and want reproducible dependency resolution.How can I help you explore Laravel packages today?