roukmoute/sqids-bundle
A Symfony bundle that integrates Sqids, letting you generate short, unique, URL-friendly IDs from numbers and decode them back. It wires Sqids into Symfony's service container for easy dependency injection and reuse across your application.
roukmoute/sqids-bundle is the Symfony bundle wrapper around sqids/sqids — the library that turns integer sequences into short, obfuscated-looking string IDs (and back). There is no Laravel service provider in this package, so in a Laravel codebase you generally get the behaviour from sqids/sqids and get the configuration patterns documented here.
First steps (Symfony host app):
composer require roukmoute/sqids-bundleRoukmoute\SqidsBundle\SqidsBundle is registered in config/bundles.php (Flex recipes may not auto-register it on older skeletons).config/packages/sqids.yaml:
sqids:
alphabet: 'abcdefghijklmnopqrstuvwxyz'
min_length: 8
blocklist: ['admin', 'cunt']
Sqids\SqidsInterface — the bundle registers the concrete implementation as a service, no manual factory needed.bin/console cache:clear) after touching the YAML.First steps (Laravel host app):
composer require sqids/sqidsAppServiceProvider:
$this->app->singleton(SqidsInterface::class, fn () => new Sqids(
alphabet: config('sqids.alphabet'),
minLength: config('sqids.min_length', 8),
blocklist: config('sqids.blocklist', []),
));
config/sqids.php mirroring the keys above.First real use case: give auto-increment primary keys a public-facing representation — /orders/86Rf07xd4z instead of /orders/42. Start by writing a HasSqids trait or an SqidsCast so models expose public_id while keeping the integer key internally.
Look first at the bundle's DependencyInjection/Configuration.php for the exact key names and defaults, and at sqids/sqids's SqidsInterface for the two-method API you'll actually call: encode(array $numbers): string and decode(string $id): array.
URL slugs / route model binding. Encode on the way out, decode in route resolution:
Route::get('/orders/{order}', function (string $order) {
$ids = app(SqidsInterface::class)->decode($order);
abort_if($ids === [], 404);
return Order::findOrFail($ids[0]);
});
Wrap this in a Route::bind or a custom cast rather than repeating it per controller.
Composite IDs. encode([$tenantId, $orderId]) packs a tenant scope into the slug; decode() gives you back both, so you can assert tenant ownership before querying. This is the main reason to prefer Sqids over Hashids-style single-value hashing — the input is a sequence, and order is significant (encode([1,2]) !== encode([2,1])).
Configuration as a service-level singleton. Build the Sqids instance once (container singleton / bind with singleton: true). Construction validates the alphabet, so you want that failure at boot, not at request time.
Environment separation. Different alphabets per environment are tempting but toxic: IDs generated in staging won't decode in production and QA links break. Drive alphabet/min_length from a single env var that is identical across environments except local, and treat it as append-only config.
Extension point for tests. Bind a deterministic alphabet (alphabet: 'abcdefghijklmnopqrstuvwxyz', min_length: 0) in the test container so assertions on generated IDs are stable, or bind a fake SqidsInterface that returns "id-{$n}" when you only care about plumbing.
Migration pattern for existing IDs. Never re-encode in place. Add a public_id column, backfill with a queued command, keep the integer column as the source of truth, and add a unique index on the string. The encoder is cheap; the backfill is the only dangerous step.
min_length invalidates every ID you've ever issued. There is no versioning in the payload. If you must rotate, version the column (public_id_v2) and keep the decoder for the old alphabet alive during a transition.decode() on garbage returns [] — but it is lenient and can happily decode a mistyped/near-miss string into a different valid number sequence. Always check count($ids) matches what you encoded, and always re-verify existence in the database rather than trusting the decode.decode(encode($n)) may not equal $n. If you rely on exact round-trips, add a test that exercises numbers whose IDs collide with your blocklist, and prefer a blocklist over a min_length hack.config:cache error, not a runtime one. Good, but confusing if you only see it in CI.min_length is a floor, not a fixed width. Short numbers still encode to exactly min_length; large composites grow past it. Don't size a DB column to min_length.loadInternal() now declares a return type. Harmless for consumers, but if you subclass Roukmoute\SqidsBundle\DependencyInjection\SqidsExtension and overrode loadInternal() without a return type, you'll hit a fatal signature error — add : void to your override. This release also signals the package tracks Symfony 8 / current PHP, so pin accordingly if you're still on Symfony 6.SqidsServiceProvider or facade. In Laravel, register the binding manually (see Getting Started) or you'll get "Target [Sqids\SqidsInterface] is not instantiable" on first resolve.$id and json_encode($sqids->decode($id)) before the lookup — nine times out of ten it's a URL-encoding issue (the default alphabet is alphanumeric and case-sensitive, so a lowercased slug silently decodes to something else).How can I help you explore Laravel packages today?