swiss-devjoy/laravel-easy-hashids
A Laravel package that simplifies Hashids: encode and decode numeric IDs into short, obfuscated strings for URLs and APIs. Includes easy configuration, helpers, and smooth integration with Laravel models and route binding.
swiss-devjoy/laravel-easy-hashids is a thin Laravel wrapper around hashids/hashids — it turns sequential integer keys into short, reversible, non-sequential-looking strings (42 → kMp4Xz) and back. It is not encryption, and that distinction drives most of the guidance below.
Install and publish config
composer require swiss-devjoy/laravel-easy-hashids
php artisan vendor:publish --tag=easy-hashids-config
The service provider and facade are auto-discovered on Laravel 5.5+, so there is nothing to register in config/app.php unless you are on an older skeleton.
Where to look first
config/easy-hashids.php — salt, minimum length, alphabet, and (from the v1.1/v1.2 line) any support for multiple named connections. Read this before anything else; almost every production issue with this package traces back to these three values.Hashids::encode().src/ for the trait (usually HasHashids or similar) and any validation rule. Those are the only two integration points beyond the facade.First use case: opaque IDs in URLs
// .env
HASHIDS_SALT=replace-with-a-long-random-string
HASHIDS_MIN_LENGTH=12
// routes/web.php
Route::get('/orders/{order}', [OrderController::class, 'show']);
// app/Models/Order.php
public function getRouteKey(): string
{
return Hashids::encode($this->getKey());
}
// resolve incoming
$id = Hashids::decode($orderHash)[0] ?? null;
abort_if($id === null, 404);
That loop — encode on the way out, decode and 404 on the way in — is 80% of day-to-day usage.
Route model binding via the package trait. Rather than hand-rolling getRouteKey() and resolveRouteBinding() on every model, use the shipped trait where it exists, or encapsulate the pair in your own base model:
public function resolveRouteBinding($value, $field = null): ?Model
{
$decoded = Hashids::decode($value);
return $decoded === [] ? null : $this->whereKey($decoded[0])->first();
}
Always return null rather than throwing so Laravel emits a clean 404.
API resources / presenters. Encode at the boundary, not in the model's serialization. Keep raw integer keys in toArray() for internal consumers and add a separate hashid field, otherwise pagination, sorting, and internal service calls all break.
return [
'id' => Hashids::encode($this->id),
'status' => $this->status,
];
Validation of inbound hashes. Wrap decoding in a rule so malformed input fails at the FormRequest layer:
Validator::extend('hashid', fn ($attr, $value) => Hashids::decode($value) !== []);
Multiple named connections / salts. If v1.2.0 exposes more than one connection, use them deliberately: one salt for public-facing URLs, a different one for tenant-scoped resources, so a leaked salt in one domain does not let an attacker enumerate the other. Do not use per-tenant salts unless you are prepared to encode with the tenant context everywhere, including queues and console commands.
Salt rotation strategy. When you rotate the salt, old links die. Either keep a deprecated connection in config and try new → old on decode, or route a ?v=2 segment — but plan it before you rotate, not after.
Bulk encoding. For index endpoints, encode in a loop or with the package's batch helper if one exists. Hashing is cheap (microseconds), so this is rarely the bottleneck; do not reach for caching unless profiling says so.
Background jobs. Encode before dispatch or decode inside the job consistently — never store a hashid as the job's identifier, because a salt change will strand queued jobs that can no longer resolve their own payload.
Hashids are reversible, not secret. Anyone who learns your salt can decode every ID. Never use this package for password resets, signed URLs (URL::signedRoute exists for that), API tokens, or anything where guessing an ID grants access you haven't independently authorized. Authorization still belongs in policies.
Changing the salt invalidates every issued link. This is the single most common production incident with hashids. Treat HASHIDS_SALT as append-only data: add new values, never overwrite them without a decode fallback.
Config caching bites during deploys. php artisan config:cache freezes the salt at build time. If you inject the salt via a runtime secret but cache config in CI, every environment silently shares the build-time salt. Cache config, but populate the env var during the cache-build step.
decode() returns an array, always. Invalid input yields [], not null or an exception. Hashids::decode($x)[0] ?? null is the safe idiom; Hashids::decode($x)[0] alone is a 500 waiting to happen.
Alphabet constraints. The alphabet must contain at least 16 unique characters, and characters must be unique. If you hand out hashids for humans to type, drop 0/O, 1/l/I — but shrinking the alphabet also shrinks the effective search space. Minimum length is cosmetic obfuscation, not a security parameter.
Zero and empty strings. encode(0) is valid and non-empty; decode('') is []. Guard against null model keys before encoding or you will produce a hashid for null and then fail to resolve it.
Big integers. PHP's hashids works with integers; extremely large IDs on 32-bit builds can lose precision. Laravel's default bigIncrements will be fine on 64-bit platforms — verify if you deploy anywhere unusual.
Hashids are not stable across alphabet/length changes. Length affects the encoding. Treat HASHIDS_MIN_LENGTH with the same append-only discipline as the salt, or accept that old links break.
Testing. Freeze the salt and length in phpunit.xml so assertions on hashid strings are deterministic:
<env name="HASHIDS_SALT" value="test-salt"/>
<env name="HASHIDS_MIN_LENGTH" value="8"/>
Use Hashids::encode($model->id) when building test URLs rather than hard-coding strings, so the tests survive a legitimate salt rotation.
Debugging tips. If a hashid "works in tinker but not in the app," you almost certainly have two different configs (cached vs. uncached, or web vs. queue worker). If a decoded ID is wrong, check for a duplicated or non-unique character in the alphabet — the library will throw on construction, but a typo'd alphabet in a stale config cache can slip through.
Check the v1.0.0 → v1.2.0 diff directly at the changelog link before upgrading. The compare URL is the release note; there is no prose description of v1.1.0 or v1.2.0 features in the announcement, so confirm whether the intervening releases added named connections, a validation rule, or a breaking change to the facade before you bump the constraint in composer.json. Pin to ^1.2 and read the diff in the same sitting.
How can I help you explore Laravel packages today?