cybercog/laravel-optimus
Laravel Optimus wraps the Optimus ID obfuscation library for Laravel, letting you encode and decode integer IDs into short, URL-friendly strings. It offers a facade and helpers, Blade directives, validation rules, route model binding, and configurable settings for hiding database IDs in URLs.
cybercog/laravel-optimus is a thin Laravel wrapper around jenssegers/optimus, which implements Knuth-style multiplicative hashing to turn sequential integers into reversible, non-sequential-looking numbers. It is obfuscation, not encryption — the mapping is deterministic and reversible by anyone holding your prime/inverse pair.
Minimal setup:
composer require cybercog/laravel-optimus
php artisan vendor:publish --tag=optimus-config
The published config/optimus.php defines one or more connections, each with a prime and inverse pair. Generate a valid pair with the prime generator shipped by jenssegers/optimus — do not hand-pick numbers, a mismatched inverse will silently decode to garbage rather than erroring.
First use case — hide auto-increment IDs at the API boundary:
use CyberCog\LaravelOptimus\Facades\Optimus;
Optimus::encode(1); // 873421 (example)
Optimus::decode(873421); // 1
Look first at: the config file (connections, prime/inverse), the Optimus facade, and the manager class it resolves. Release 3.13.0 is a compatibility release — it widens the illuminate/* constraints to Laravel 13 and adds repo docs/tooling only. No API changes, no config changes, no migration needed from 3.12.x.
Encode at the serialization boundary, decode at the input boundary. Don't sprinkle Optimus::encode() through controllers and models; pick two seams.
API resources (outbound):
class OrderResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => Optimus::encode($this->id),
// ...
];
}
}
Route model binding (inbound) — the cleanest single integration point, since it makes every /{order} route work without touching controllers:
class Order extends Model
{
public function getRouteKey(): mixed
{
return Optimus::encode($this->getKey());
}
public function resolveRouteBinding($value, $field = null): ?Model
{
return $this->where($this->getKeyName(), Optimus::decode($value))->first();
}
}
Guard the decode: a non-numeric or out-of-range segment should 404, not blow up. Wrap in a try/catch or a ctype_digit() check and abort(404).
Validation rule. Validate incoming encoded IDs once, centrally, instead of decoding defensively everywhere:
Validator::extend('optimus', fn ($attr, $value) => is_numeric($value) && Optimus::decode((int) $value) > 0);
Multiple connections for multi-tenancy. Define a connection per tenant (or per domain) in optimus.php and select at runtime with Optimus::connection('tenant_b'). This lets you rotate a single tenant's key space without invalidating everyone else's URLs.
Batch operations. encode() is O(1) arithmetic, so mapping a collection is fine, but for large exports prefer ->map(fn ($id) => Optimus::encode($id)) over per-item facade resolution in a tight loop.
Where it fits day-to-day: public-facing URLs (share links, invoice numbers, unsubscribe tokens), cursor pagination keys exposed to clients, and any place where sequential IDs leak business volume. Keep it out of internal joins, foreign keys, and anything you'd need to query by — you'd lose the index.
orders_v2) and keep the old connection alive for a deprecation window.config:cache matters. Because the pair lives in config, run php artisan config:clear && php artisan config:cache on deploy. A stale cached config after rotation is a classic "works locally, 404s in prod" bug.decode() is not a validator. It happily returns a number for arbitrary input. Always range-check the result against the actual record lookup, and never let a decoded value bypass authorization — obfuscated IDs are still guessable-ish and enumerable in principle.null and 0 explicitly. Optimus::encode(null) or encoding 0 produces confusing output; short-circuit with a ternary at the resource layer.illuminate/* ^13 constraint. If you're upgrading the framework, also confirm your other ID/route-binding packages widened their constraints, otherwise Composer will pin you back to Laravel 12. Nothing in this release requires code changes.How can I help you explore Laravel packages today?