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

Hashids Bundle Laravel Package

roukmoute/hashids-bundle

Symfony bundle integrating Hashids to encode and decode numeric IDs into short, unique, obfuscated strings. Configure salt, length, and alphabet, then use services or Twig filters to hide database IDs in URLs.

View on GitHub
Deep Wiki
Context7

Getting Started

roukmoute/hashids-bundle is a Symfony bundle, so in a Laravel codebase you almost never install it directly. What you actually consume day-to-day is the underlying hashids/hashids library, plus the patterns this bundle popularises: a single configured hashids service, and an argument resolver that transparently converts a hashed route segment back into a real entity/ID. This v4.0 release only changes the second half of that (the resolver), so the patterns are what matter to you.

First steps in a Laravel app:

  1. composer require hashids/hashids (the bundle's composer.json on v4.0.0 requires Symfony 6.2+ / PHP 8.1+, so pulling the bundle into Laravel isn't a thing).
  2. Publish a config file with your salt, minimum length and alphabet:
// config/hashids.php
return [
    'default' => [
        'salt'      => env('HASHIDS_SALT'),
        'min_length'=> 12,
        'alphabet'  => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890',
    ],
];
  1. Bind one shared instance in a service provider:
$this->app->singleton(HashidsInterface::class, fn () => new Hashids(
    config('hashids.default.salt'),
    config('hashids.default.min_length'),
    config('hashids.default.alphabet'),
));
  1. First use case: stop leaking sequential IDs in public URLs and API payloads — /orders/9xK2mQpLwR instead of /orders/1042, and "id" => "9xK2mQpLwR" in resources.

If you are running a Symfony 6.2+ service next to your Laravel app, the mapping of concepts is: hashids service → your singleton binding, #[Hashids] route attribute → route model binding / form-request normalisation.

Implementation Patterns

Eloquent cast + getRouteKey() — the highest-leverage pattern, and the closest analogue to the bundle's resolver:

class HashidCast implements CastsAttributes
{
    public function get($model, $key, $value, $attributes)
    {
        return $value === null ? null : app(HashidsInterface::class)->encode($value);
    }

    public function set($model, $key, $value, $attributes)
    {
        return [$key => app(HashidsInterface::class)->decode($value)[0] ?? $value];
    }
}

Pair it with getRouteKey() returning the hashed value, and resolveRouteBinding() decoding it back.

Route model binding for endpoints that aren't model-backed:

Route::bind('order', function (string $value) {
    $id = app(HashidsInterface::class)->decode($value)[0] ?? null;
    abort_if($id === null, 404);
    return Order::findOrFail($id);
});

Request normalisation — the Laravel equivalent of the bundle's argument resolver. Decode in prepareForValidation() so downstream validation rules and controllers see raw integers:

protected function prepareForValidation(): void
{
    $this->merge([
        'order' => app(HashidsInterface::class)->decode($this->route('order'))[0] ?? null,
    ]);
}

Validation rule for inbound hashes (HashidExists / ValidHashid) — fails fast with a 422 instead of a 500 from findOrFail on garbage input.

API resources — encode in toArray(), never expose raw IDs. Keep the encode in one place so a future salt rotation is a one-line change.

Multiple connections / salts — mirror the bundle's multi-connection config with named bindings (hashids.public, hashids.internal). Public-facing tokens and internal job references should never share a salt.

Testing helper — a hashid(1) test helper or factory state so tests read get("/orders/".hashid($order->id)) rather than hard-coding opaque strings.

If you forked the bundle's resolver into a Laravel package, mirror the v4 change: implement the new value-resolver contract and keep the class name stable so decorators keep working.

Gotchas and Tips

v4.0 is a breaking change, but scoped. The migration from ArgumentValueResolverInterface to ValueResolverInterface means:

  • Minimum Symfony is now 6.2; 5.4 LTS and 6.0/6.1 are out. Any Laravel-adjacent service still on those versions cannot upgrade.
  • Anything that extends or decorates HashidsValueResolver (custom resolvers, compiler passes, decorates: service definitions) breaks on a signature-level change, not silently — but it breaks at container compile time, which is easy to miss in CI if you only run unit tests.
  • PHP floor is now 8.1. If your composer.json doesn't pin php, run composer why-not php 8.0 before bumping.

decode() returns [], not null. This is the single most common production bug. $hashids->decode($bad)[0] throws "Undefined array key 0" and 500s on any malformed URL. Always ?? null and abort(404).

Salt is configuration, not encryption. Hashids is reversible and the output space is enumerable. Never use it as an authorisation token, signed URL substitute, or password reset secret. Use URL::signedRoute() for that.

Salt rotation is a breaking change for users. Every URL in emails, sitemaps, invoices, QR codes and third-party integrations dies at once. Treat the salt like an app key: set it once, store it in the secret manager, and if you must rotate, keep the old instance bound as hashids.legacy for a decode-only grace period.

Alphabet constraints. Minimum 16 unique characters, no spaces. If you customise it, remember route regex constraints — the default alphabet is alphanumeric, but if you introduce - or _ and your route is ->where('id', '[A-Za-z0-9]+'), decoding never runs. Keep the default unless you have a reason.

Environment drift is the usual culprit for "links work in staging, 404 in prod": different HASHIDS_SALT between environments, or a trailing newline from a pasted env var. trim() your env value.

Unsaved models. $model->id is null before save(), so the cast returns null and your URLs render /orders/. Guard getRouteKey() or generate URLs after persist only.

Extension point — bind your own implementation of the encoder interface in the container and alias it; in Symfony this is HashidsInterface service aliasing, in Laravel it's $this->app->bind(). Keep the interface narrow (encode/decode) so you can swap in a different obfuscator (Sqids is the actively maintained successor to Hashids) without touching call sites.

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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core