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 Laravel Package

torann/hashids

Laravel wrapper for Hashids that encodes numeric IDs into short, unique, non-sequential strings and decodes them back, helping hide database IDs in URLs and APIs with configurable salt, length, alphabet, and simple Laravel integration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish the config.

composer require torann/hashids
php artisan vendor:publish --provider="Torann\Hashids\HashidsServiceProvider"

Auto-discovery registers the provider and Hashids facade — no manual wiring. The publish drops config/hashids.php, which is the entire public API surface you need to read. Set the salt from env, never hardcode it:

// config/hashids.php
return [
    'default' => 'main',
    'connections' => [
        'main' => [
            'salt'     => env('HASHIDS_SALT', ''),
            'length'   => env('HASHIDS_LENGTH', 12),
            'alphabet' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890',
        ],
    ],
];
HASHIDS_SALT=base64:...   # generate once per environment, store in secret manager
HASHIDS_LENGTH=12

Where to look first. Read the published config, then the facade — encode(), decode(), encodeHex(), decodeHex(), connection(). That is the whole learning surface. Everything else you'll do is application-side plumbing at the serialization boundary.

First use case — hide sequential IDs on one public surface. An order reference in a URL, a share link, a tracking number. Encode outbound, decode inbound:

use Torann\Hashids\Facade\Hashids;

Hashids::encode(10432);         // "k4Lx9mQ2bA7e"
Hashids::decode('k4Lx9mQ2bA7e'); // [10432]  — always an array

Do this before you go further: wrap it. Don't scatter Hashids:: calls across controllers. A two-method adapter pays for itself the first time you rotate a salt or swap to ULIDs:

namespace App\Support;

use Torann\Hashids\Facade\Hashids;

class PublicId
{
    public static function encode(int|string $id, ?string $connection = null): string
    {
        return $connection
            ? Hashids::connection($connection)->encode($id)
            : Hashids::encode($id);
    }

    public static function decode(string $hash, ?string $connection = null): ?int
    {
        $decoded = $connection
            ? Hashids::connection($connection)->decode($hash)
            : Hashids::decode($hash);

        return $decoded[0] ?? null;   // [] on garbage input
    }
}

Implementation Patterns

Pattern 1 — route keys (the workhorse). Override two methods on models with a public surface; you get hashid URLs and automatic decoding everywhere route model binding is used, with no controller changes:

trait HasPublicId
{
    public function getRouteKey(): string
    {
        return PublicId::encode($this->getKey());
    }

    public function resolveRouteBinding($value, $field = null): ?static
    {
        $id = PublicId::decode($value);

        return $id ? $this->whereKey($id)->first() : null;   // null => 404
    }
}

Apply selectively — Order, Invoice, Share — never on join tables, internal models, or admin-only resources. Every extra encode is indirection with no user-visible payoff.

Pattern 2 — API resources. When you want a record publicly addressable but internally integer-keyed, encode in the resource, not the model:

public function toArray($request): array
{
    return [
        'id'        => PublicId::encode($this->id),
        'reference' => $this->reference,
        // never emit raw $this->id alongside the encoded one
    ];
}

Shipping both defeats the purpose — someone will build against the integer.

Pattern 3 — per-domain connections. Different resources, different salts and lengths. Coupons want short and human-typeable; tenant IDs want longer:

'connections' => [
    'main'    => ['salt' => env('HASHIDS_SALT'),         'length' => 12],
    'coupons' => ['salt' => env('HASHIDS_SALT_COUPONS'), 'length' => 8],
],
PublicId::encode($order->id);              // default connection
PublicId::encode($coupon->id, 'coupons');  // short code

A coupon hashid decodes to garbage against main — that isolation is free.

Pattern 4 — form requests. Validate the identifier shape early so bad input is a 422 rather than a null-dereference three layers down:

'order' => ['required', 'string', function ($attr, $value, $fail) {
    if (PublicId::decode($value) === null) {
        $fail('Invalid order reference.');
    }
}],

Pattern 5 — don't persist hashids. Storing them in a column is a smell; they're derived values. Encode at the boundary, decode immediately, keep the DB normalized. The one legitimate exception is a denormalized public_ref column for records that must survive a salt rotation — a deliberate, documented decision, not a convenience.

Pattern 6 — deterministic tests. Pin the salt in phpunit.xml; a random salt per run makes assertions impossible:

<env name="HASHIDS_SALT" value="test-salt-fixed"/>
<env name="HASHIDS_LENGTH" value="10"/>

Assert round-trips, not literals: assertSame(42, PublicId::decode(PublicId::encode(42))) survives rotation, "k4Lx9mQ2bA7e" does not. Use Hashids::shouldReceive() when you need to force a decode failure.

Pattern 7 — rolling out on an existing app. Don't cut over. Add the trait, emit encoded URLs, and keep integer URLs alive via a redirect route that decodes the integer and 301s to the encoded form. Watch 404 rates and legacy-URL hits; retire the redirect when the tail flattens. Emails, bookmarks, and cached pages carry the old form for months.

Pattern 8 — batch encode/decode. encode(1, 2, 3) returns one string, decode() returns [1, 2, 3]. Handy for compound public references (account+invoice) — but treat it as opaque and always guard the array access.

Gotchas and Tips

It is obfuscation, not authorization, and not a secret. Anyone with the salt decodes everything. A leaked salt (repo, stack trace, config:cache artifact) makes every ID enumerable. Never use hashids as a capability token, invitation link, reset link, or signed-URL substitute. If unguessability is required, use UUIDv7/ULID or a server-side random token. This is the one people get wrong after a week of smooth sailing.

decode() always returns an array — [] on failure. No exception, no false, no null. Hashids::decode('garbage')[0] is an Undefined array key 0 warning waiting to become an error page. Always go through the adapter and always null-check before findOrFail.

length is a minimum, not a fixed width. It pads, never truncates, and grows with input. A char(10) column, a {10} regex, or a fixed-width UI assumption will break at volume. Treat hashids as variable-length strings in schema, validation, and display.

alphabet needs ≥16 unique characters, no duplicates. The same alphabet must decode what it encoded. Changing it in production kills every outstanding link — identical blast radius to salt rotation, which nobody expects. Validate a custom alphabet at boot in a test; the failure mode is an exception on the first request after deploy.

Salt rotation is a product event, not a deploy. Rotating invalidates every outstanding link at once: shared URLs 404, emailed references break, cached pages point nowhere. Decide up front where the salt lives, who can rotate it, whether you need dual-read via a second named connection during transition, and what the user-facing recovery path is. Write it down.

env() only works inside config files. Hashids::connection(['salt' => env('...')]) at runtime returns null the moment php artisan config:cache runs — and it works locally, so it ships. Keep every env lookup in config/hashids.php, and re-run php artisan config:cache after changing the salt (your pipeline should, but verify).

Connections are namespaced; the default is default. Hashids::connection('coupons') and Hashids::encode() are different domains producing incompatible output. Pick a convention once — pass the connection name explicitly inside the adapter, defaulting to main — and never let a controller guess.

The wrapper is thin — price in the risk. ~54 stars, one maintainer, no listed dependents. The BSD-2 license and tiny surface make the fallback cheap: call hashids/hashids directly, or fork the glue (a provider, a facade, a config array). Write the fallback down before adoption so it's a known option during an incident, not a discovery. Your adapter boundary is what makes it a one-file change.

Verify the framework constraint; don't trust the release note. 3.0.6 claims Laravel 12 support — confirm the illuminate/* constraint in composer.json actually allows the version you deploy and your PHP floor. It should be additive to 11, but a mixed-version fleet needs that answered before rolling forward.

Debugging decode failures. [] is silent. When a link 404s, check in order: (1) config:cache stale after a salt change, (2) wrong connection name in the call site, (3) alphabet or length changed on one side, (4) the ID was never valid (user typo). Log the raw input and the connection used at the boundary — five minutes of logging beats an afternoon of guessing.

Extension points, in usefulness order: (1) the adapter class — the only one that matters long-term; (2) named connections — per-resource salts and lengths with no code change; (3) the config file — salt, length, alphabet, nothing more. Need separators, guards, or wider hex/string encoding beyond the two facade methods? Drop to the underlying hashids/hashids library through your adapter rather than fighting the wrapper. That's precisely what the boundary is for.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle