elfsundae/laravel-hashid
Laravel Hashids turns auto-increment integers into short, unique, non-sequential strings for safe public IDs in URLs. Includes Eloquent route binding, a facade, validation rules, and configurable salt, length, and alphabet.
1. Install and confirm auto-discovery
composer require elfsundae/laravel-hashid
On Laravel 5.5+ the service provider and Hashid facade are auto-discovered. If you're on an older version, register Elfsundae\LaravelHashid\HashidServiceProvider and alias Elfsundae\LaravelHashid\Facades\Hashid.
2. Publish and review the config
php artisan vendor:publish --provider="Elfsundae\LaravelHashid\HashidServiceProvider"
Open config/hashid.php first. It defines a default connection plus a connections map. Each connection controls the salt, minimum hash length, and alphabet. Treat this file as the source of truth for every hash your app emits.
3. Set a real salt
HASHID_SALT=some-long-random-per-app-secret
Do not ship the empty/default salt. The salt is the only thing standing between a hash and the original integer.
4. First use
use Elfsundae\LaravelHashid\Facades\Hashid;
$hash = Hashid::encode(42); // e.g. "kM"
$ids = Hashid::decode($hash); // e.g. [42] — hashids decode returns an array
$id = $ids[0] ?? null;
Or via the helper:
hashid()->encode(42);
hashid('orders')->encode(42); // named connection
Your first real use case should be an API resource or model accessor that swaps a sequential primary key for a hashid in responses.
Presentation-layer encoding with API resources
Keep database keys as integers; encode only at the boundary.
class OrderResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => hashid('orders')->encode($this->id),
'created_at' => $this->created_at,
];
}
}
Model accessor for URLs and Blade
class Order extends Model
{
protected $appends = ['hashid'];
public function getHashidAttribute(): ?string
{
return $this->exists ? hashid('orders')->encode($this->getKey()) : null;
}
}
Append only when needed — it adds work to every serialization.
Route model binding
Implicit binding matches the raw key, so override it to decode first:
public function resolveRouteBinding($value, $field = null)
{
$decoded = hashid('orders')->decode($value);
abort_if(empty($decoded), 404);
return $this->where($field ?? $this->getRouteKeyName(), $decoded[0])
->firstOrFail();
}
Then route with {order} and receive hashed strings in URLs without changing controller code.
Validation rule for incoming hashes
Validator::extend('hashid', fn ($attribute, $value) =>
! empty(hashid()->decode($value))
);
$request->validate([
'order_id' => ['required', 'hashid'],
]);
Or normalize in a form request:
protected function prepareForValidation()
{
$decoded = hashid()->decode($this->input('order_id'));
$this->merge(['order_id' => $decoded[0] ?? null]);
}
One connection per domain
Use separate salts for different entity types so an order hash can't accidentally resolve as a user ID:
'connections' => [
'main' => ['salt' => env('HASHID_SALT'), 'length' => 8],
'orders' => ['salt' => env('HASHID_ORDERS_SALT'), 'length' => 8],
'users' => ['salt' => env('HASHID_USERS_SALT'), 'length' => 8],
],
UUIDs and hex values
Hashids operates on integers. For UUIDs, strip dashes and use the hex API:
$hash = Hashid::encodeHex(str_replace('-', '', $uuid));
$hex = Hashid::decodeHex($hash);
$uuid = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split($hex, 4));
Middleware for route-prefix decoding
If many routes carry hashed IDs, a middleware that decodes known route parameters (and 404s on failure) keeps controllers clean:
public function handle($request, Closure $next)
{
foreach (['order', 'user'] as $param) {
if ($value = $request->route($param)) {
$decoded = hashid()->decode($value);
abort_if(empty($decoded), 404);
$request->route()->setParameter($param, $decoded[0]);
}
}
return $next($request);
}
Testing
Prefer round-trip assertions over hard-coded hashes, since changing salt or length changes every output:
$this->assertSame(42, hashid()->decode(hashid()->encode(42))[0]);
$this->assertEmpty(hashid()->decode('not-a-real-hash'));
decode() returns an array (hashids semantics). Always index [0] and guard with empty()/??. A tampered or foreign hash usually yields [], not an exception.length => 8 makes hashes look uniform; it does not make them harder to reverse. Security comes from salt secrecy.0/O, 1/l) is fine, but changing the alphabet later breaks existing hashes.PHP_INT_MAX and UUIDs need encodeHex/decodeHex.null to findOrFail, you'll get the right status; if you cast blindly, you'll get a type error.config/hashid.php or .env, run php artisan config:clear (and config:cache in production). Stale cached config with an old salt is a classic "why did all my links break" bug.encrypt, Hash::make, signed URLs). Hashids is obfuscation.How can I help you explore Laravel packages today?