jasonbradley/hash-ids-bundle
Symfony bundle wrapping the hashids library to encode numeric IDs into short, reversible, non-sequential hashes—useful for hiding database IDs in URLs and public APIs. Adds service and Twig integration for encoding and decoding throughout a Symfony app.
Architecture fit — poor, and mislabeled. Despite the request framing this as a Laravel/PHP package, jasonbradley/hash-ids-bundle is a Symfony2 bundle, not a Laravel package. It ships a Bundle subclass and a DependencyInjection extension registered through Symfony's kernel — neither of which Laravel's service container consumes. There is no ServiceProvider, no config/*.php, no facade. Installing it in a Laravel app gives you an inert vendor directory. The only genuinely reusable asset is its transitive dependency on hashids/hashids, which is a framework-agnostic PHP library that Laravel can use directly.
Integration feasibility — low as-is, high if you skip the package. Two viable paths:
| Path | Effort | Verdict |
|---|---|---|
| Install the bundle and shim Symfony DI into Laravel | Multi-day, brittle, drags in symfony/http-kernel |
Reject |
composer require hashids/hashids, wrap in a ~60-line service provider + facade + Eloquent cast |
0.5–1 dev-day | Recommended |
composer require sqids/sqids, same wrapper |
0.5–1 dev-day | Recommended if greenfield |
Functionally the bundle offers nothing the underlying library does not — it is a container-registration wrapper for a stateless pure function.
Technical risk — high, concentrated in abandonment. Release history ends 2015-08-05, which implies a Symfony 2.x-era composer.json with PHP ~5.3/~5.4 constraints and no ^7/^8 support. Modern Laravel (10/11/12) runs PHP 8.1+, so composer install will either fail the platform check or require --ignore-platform-reqs, at which point you are running unmaintained 2015 code on PHP 8 with zero test coverage against your runtime. Combined signals: 0 stars, 0 dependents, score 0.13. There is no evidence of a second pair of eyes on this code, no security disclosure path, and no human to merge a patch. For code on the request path of every public URL, that is an unacceptable supply-chain posture for a feature whose entire value is a few lines of hashing.
Separately, evaluate the underlying concept rather than just the wrapper: Hashids is reversible obfuscation, not security. Anyone with the salt can enumerate. It also does not prevent enumeration if your IDs are dense and the salt leaks — and salt leakage via a committed .env or a stack trace is the common failure. If the requirement is "unguessable," Hashids/Sqids is the wrong tool; use a random opaque token column with a unique index.
Key questions for the product owner:
id=1) or security (prevent enumeration/scraping)? Different answers change the design entirely.Stack fit. Poor at the package level, good at the library level. Laravel's container, config repository, and Eloquent casting are all sufficient to host Hashids cleanly; the Symfony bundle abstraction adds nothing and costs a heavy transitive dependency (symfony/http-kernel) inside a Laravel app.
Migration path. Treat this as a build, not an adopt:
composer.json. Record the decision and rationale in the ADR so a future engineer doesn't rediscover the repo and try again.hashids/hashids (or sqids/sqids) as a direct dependency — one package, no framework coupling.config/hashids.php with salt, min_length, alphabet, sourced from env. Fail fast at boot if salt is empty in non-local environments.PublicIdEncoder interface so call sites stay testable and the vendor is swappable.public_id) and a route-binding resolver so controllers receive the model, not the hash string.Compatibility. The bundle's Symfony2/PHP 5.x constraints are the blocker; the library underneath runs fine on PHP 8. Verify the exact hashids/hashids major version you land on — the min_length/alphabet API and decode-failure semantics (empty array vs. exception) have shifted across majors, and a silent behavior change here produces dead links, not a test failure, unless you write the contract test.
Sequencing. Ship in this order to cap blast radius:
public_id, inbound routes still accept integers. Nothing can break.Maintenance — the dominant cost driver. Owning a ~60-line wrapper is less total maintenance than owning a vendored 2015 bundle you must patch for PHP 8, because the wrapper is code you wrote and understand, while the bundle is code nobody maintains. Budget roughly 2–4 hours per year for the wrapper (dependency bumps) versus an open-ended liability for the bundle on every PHP or Laravel major upgrade. Note the underlying hashids/hashids is itself in low-activity maintenance and has a designated successor (sqids/sqids); if this feature is strategic, start on Sqids and skip the eventual second migration.
Support. No upstream support exists — 0 stars, 0 dependents, last commit a decade ago. Any defect becomes an internal ticket with an internal owner. This is acceptable for a well-tested 60-line wrapper; it is not acceptable for third-party code you cannot read in one sitting. Assign a named owner in the team's service catalog either way.
Scaling. Effectively a non-issue. Encoding is CPU-bound, O(n) in the integer's digits, sub-microsecond, in-process, no I/O, no external service, no shared state. It will not be a bottleneck at any plausible request volume. The real scaling constraint is entropy collapse: Hashids with a short min_length on a densely-allocated integer space produces short, guessable-looking strings. If enumeration resistance matters, raise min_length — which is a config change that breaks every previously issued link. Decide min_length once, before launch, and treat it as immutable.
Failure modes to design against:
| Failure | Symptom | Mitigation |
|---|---|---|
| Salt rotation | Every historical link 404s; silent, total, irreversible | Salt in secrets manager, versioned (salt_v1, salt_v2) with dual-decode during rotation windows |
| Salt committed to repo or leaked in a stack trace | Full enumeration of all records | Never echo salt; redact in exception handlers; treat as a sev-2 incident |
min_length/alphabet change |
Same as salt rotation | Pin in config; add a CI test asserting stable output for fixture IDs |
Decode failure returns empty/null |
Route binding 404s or, worse, binds the wrong model | Explicit try/catch → 404; never (int) cast a failed decode to 0 |
| Hashids used for authorization | Users reach other tenants' records when salt is shared | Hashids is never an access control; enforce ownership on the integer model lookup |
| Hashids used as a cache or idempotency key | Key instability across config changes | Forbid by convention; keys stay integer |
Ramp-up. Low. A mid-level Laravel engineer with no prior Hashids exposure can implement and test the wrapper in under a day given an existing service-provider pattern in the codebase. The learning cost is not the library — it is the operational discipline around salt immutability and link permanence, which is a 30-minute design conversation, not a training program. The bundle itself offers no ramp-up advantage, since using it correctly still requires understanding the same library.
Recommendation. Do not adopt jasonbradley/hash-ids-bundle. It is a 2015 Symfony2 container wrapper with no maintainers, no consumers, and no compatibility with the target stack. Deliver the underlying capability — short public identifiers — by depending on the Hashids (or Sqids) library directly behind a thin, tested, internally-owned Laravel service provider, with the salt in a secrets manager and a CI-enforced output-stability test. Estimated cost: 1–2 dev-days end to end, versus an open-ended platform-upgrade liability for a package that provides no functionality you cannot write in an afternoon.
How can I help you explore Laravel packages today?