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

Hashid Bundle Laravel Package

pgs-soft/hashid-bundle

Symfony bundle that integrates Hashids to encode and decode numeric IDs, helping obfuscate database identifiers in URLs. Provides a configurable service and Twig filters for generating short, unique hashes in templates and routes, with simple bundle configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install

composer require pgs-soft/hashid-bundle

It pulls in hashids/hashids as the underlying encoder. Symfony Flex auto-registers PGSSoft\HashIdBundle\PGSSoftHashIdBundle; on older skeletons add it manually to config/bundles.php.

2. Configure the salt first

# config/packages/pgs_soft_hash_id.yaml
pgs_soft_hash_id:
    salt: '%env(HASHID_SALT)%'
    min_hash_length: 8
    alphabet: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'

Put the salt in .env / secrets, not in the committed YAML. Generate it once with openssl rand -hex 32 and never change it afterwards — every existing URL depends on it.

3. Where to look first

  • php bin/console debug:container hash_id (or debug:container --tag=twig.extension) to confirm the service ID and Twig filter name in your installed version.
  • vendor/pgs-soft/hashid-bundle/ — read the bundle extension class for the exact config keys and the Twig extension for the filter names. This package is small enough to read end-to-end in ten minutes.
  • php bin/console debug:config pgs_soft_hash_id to see resolved values.

4. First use case: hide entity IDs in a detail route

Twig — encode on the way out:

<a href="{{ path('app_order_show', {id: order.id|hashid}) }}">Order #{{ order.reference }}</a>

Controller — decode on the way in. The idiomatic pattern is the bundle's ParamConverter, which turns the route parameter back into an integer before your action runs:

#[Route('/orders/{id}', name: 'app_order_show')]
public function show(#[HashId] int $id, OrderRepository $orders): Response
{
    return $this->render('order/show.html.twig', ['order' => $orders->find($id)]);
}

If you prefer explicit code (and you probably should in a codebase with more than one developer), inject the service and decode yourself:

public function show(string $id, HashId $hashId, OrderRepository $orders): Response
{
    $ids = $hashId->decode($id);
    if ($ids === []) {
        throw $this->createNotFoundException();
    }
    // ...
}

That's the whole loop: encode at render time, decode at request time. Nothing in the database changes.

Implementation Patterns

Encode at the boundary, never in the domain. Keep integers as integers in entities, repositories, and service signatures. Encode only when producing output (Twig, JSON responses, generated URLs) and decode only when consuming input (controllers, API request DTOs). The moment encoded strings leak into your persistence layer or into a find($id) call you have a bug factory.

API resources. For JSON APIs, encode in the serializer/transformer layer rather than in entities:

final class OrderNormalizer implements NormalizerInterface
{
    public function __construct(private HashId $hashId) {}

    public function normalize($order, string $format = null, array $context = []): array
    {
        return [
            'id' => $this->hashId->encode($order->getId()),
            // ...
        ];
    }
}

Symfony's Serializer will happily call getId() and emit a raw integer otherwise — a very common leak.

Resolving decoded values into entities. A small resolver service keeps controllers clean and gives you one place to decide between 404 and 400:

final class IdResolver
{
    public function __construct(private HashId $hashId) {}

    public function resolve(string $hash): int
    {
        $ids = $this->hashId->decode($hash);
        if (count($ids) !== 1) {
            throw new BadRequestHttpException('Malformed identifier.');
        }
        return $ids[0];
    }
}

Use BadRequestHttpException for malformed hashes and NotFoundHttpException after a failed repository lookup — this keeps your error semantics honest and avoids leaking whether an ID exists.

Route generation. Wrap path() usage or use a Twig macro so encoding is consistent:

{% macro orderUrl(order) %}
    {{ path('app_order_show', {id: order.id|hashid}) }}
{% endmacro %}

Form handling. Encoded IDs arriving in a form field need a DataTransformer so the form component sees integers:

final class HashIdTransformer implements DataTransformerInterface
{
    public function __construct(private HashId $hashId) {}

    public function transform($value): string
    {
        return $value === null ? '' : $this->hashId->encode($value);
    }

    public function reverseTransform($value): ?int
    {
        $ids = $this->hashId->decode((string) $value);
        return $ids[0] ?? null; // let @Assert\NotBlank / NotNull report the failure
    }
}

Multi-tenant / per-environment salts. If you run several brands from one codebase, register the encoder as a non-shared service or build a small factory that constructs Hashids instances per tenant. The bundle ships one configured instance; treat it as a convenience wrapper, not the only way to get one.

Migration path. When you cannot change existing URLs, accept both forms temporarily:

$id = ctype_digit($raw) ? (int) $raw : $this->hashId->decode($raw)[0] ?? throw new NotFoundHttpException();

Emit only encoded URLs from day one so the legacy branch can be deleted after one release cycle.

Gotchas and Tips

Hashids is obfuscation, not encryption. Do not use it for password reset tokens, signed download links, or anything where guessing an ID grants privilege you wouldn't grant anyway. Your authorization layer must still check ownership on every request. The salt being "secret" gives you no cryptographic guarantee — it only makes enumeration harder.

The salt is a one-way door. Rotating it invalidates every URL anyone has bookmarked, emailed, or linked. Treat it like an app secret in infrastructure (secrets manager, per-environment), document it, and back it up with the rest of your config.

Empty array means invalid — but also check cardinality. decode() returns [] for garbage. Always guard before indexing:

$ids = $this->hashId->decode($hash);
if ($ids === []) { throw ... }

Some inputs decode to more than one integer (hashids supports encoding arrays). Unless you deliberately encode tuples, reject count($ids) !== 1.

Alphabet constraints bite. The alphabet must be at least 16 unique characters, and characters are case-sensitive. Removing 0/O/1/l/I for readability is fine, but reducing the alphabet below the minimum throws at construction — which means the failure surfaces at container boot, not at runtime. Good, but confusing the first time you see it.

min_hash_length is not a security dial. It pads output length; it does not add entropy. Set it to something like 8 to avoid two-character IDs, but don't assume a large value makes IDs unguessable. The salt is what does that.

ParamConverter caveats. The bundle's converter handles a single route parameter. For routes with two encoded segments (/orders/{orderId}/items/{itemId}), you'll either register two conversions or just decode manually in the action. Prefer the manual route once you have more than one — it's more predictable and easier to unit test. Also note the annotation-vs-attribute split: if you're on PHP 8 + Symfony 6, confirm the bundle exposes an #[HashId] attribute in your installed version; older releases only ship the Doctrine annotation class, and you may need sensio/framework-extra-bundle (removed from the default skeleton in Symfony 6) for annotation support.

Route requirements won't validate hashids. requirements: {id: '\d+'} now rejects every valid URL. Drop numeric requirements on encoded parameters and let the decoder be the validator.

Maintenance risk is real. Last release April 2023, ~13 stars, zero dependents. Before adopting, run the test suite against your Symfony version and check the repo's issue tracker. Mitigate by depending on a thin interface of your own (App\IdEncoder) implemented by the bundle service — if the bundle stalls, swapping to plain hashids/hashids is a one-file change rather than a repo-wide find-and-replace.

Cache warmup and prod debugging. The service is cheap to construct and deterministic, so caching is unnecessary; but if you see "decode returns empty for a value that works in dev," you almost certainly have a different salt loaded in prod. debug:config pgs_soft_hash_id is the fastest way to confirm.

Don't log decoded IDs alongside the hash. Half the value of the exercise is that logs and analytics no longer contain sequential customer numbers. Pick one representation per log line.

Extension point. If you need custom behavior (case-insensitive alphabets, prefixes like ord_), subclass or decorate the bundle's HashId service rather than editing vendor code:

services:
    App\HashId\PrefixedHashId:
        decorates: 'hash_id' # confirm the exact ID via debug:container
        arguments: ['@.inner']

Test helper. Add a tiny test utility that encodes fixtures so integration tests stay readable:

$url = '/orders/' . self::container()->get(HashId::class)->encode($order->getId());

Hardcoding hash strings in tests means every salt change rewrites your suite.

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.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect