tuupola/base62
PHP library for base62 encoding and decoding, handy for short URLs and compact IDs. It encodes integers and binary data using GMP, BCMath, or a pure PHP fallback, so it works without extensions, and also supports base36, base58, and other bases through a simple, consistent API.
Install with Composer — the package has no Laravel service provider, so it's a plain library you wire up yourself:
composer require tuupola/base62
The entry point is Tuupola\Base62. The constructor accepts an options array where characters sets the alphabet (default is 0-9A-Za-z) and encoder selects the backend (gmp, bcmath, php, or integer).
use Tuupola\Base62;
$base62 = new Base62(); // defaults to GMP if the extension is present
$encoded = $base62->encode(123456789); // "8m0Kx"
$decoded = $base62->decode('8m0Kx'); // "123456789"
Look first at the README's encoder matrix — the correct starting choice is dictated by whether your data is an integer (IDs, counters) or arbitrary binary (UUIDs, hashes, blobs). For a Laravel app the most common first use case is obfuscating auto-increment IDs in URLs: encode the model key on the way out, decode on the way in. Register the instance once so every controller, model accessor, and job uses the same alphabet.
Singleton binding in a service provider. Base62 is stateless, so register it once and inject it rather than new-ing it everywhere:
$this->app->singleton(Base62::class, fn () => new Base62([
'characters' => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
'encoder' => Base62::GMP,
]));
Route model binding via a custom resolver. Keep numeric IDs in the database and encode only at the boundary. A resolveRouteBinding override on the model (or a getRouteKey/resolveRouteKey trait) gives you short URLs without schema changes:
public function getRouteKey() { return app(Base62::class)->encode($this->getKey()); }
public function resolveRouteBinding($value, $field = null) {
$id = app(Base62::class)->decode($value);
return $this->where($this->getKeyName(), $id)->firstOrFail();
}
Binary-safe tokens and short codes. When you need compact opaque values (invite codes, API tokens, share links), feed random_bytes() output into encode() — Base62 handles arbitrary byte strings, not just integers, so 16 raw bytes become ~22 chars. Decode returns the original byte string; store the hash of the decoded value if you need lookup, since encoded output isn't searchable against a numeric index.
Encoding before insert vs. after fetch. Two viable workflows: (a) store the raw integer and encode on read — flexible, no migration; (b) store the base62 string in a dedicated public_id column with a unique index — good when the value is exposed externally and you want to query by it directly. Option (b) needs the column to be VARBINARY/utf8mb4_bin because Base62 is case-sensitive (see Gotchas).
API resources. Centralize encoding in an ApiResource or a Base62Cast custom cast so responses are consistent and you never leak raw IDs. A cast that encodes on set/get lets models expose public_id transparently.
Custom alphabets. Pass characters to reorder or exclude confusable glyphs (0/O, 1/l) for human-typed codes — just ensure it's exactly 62 unique characters and consistent across every instance in the app, or decodes will silently produce garbage.
a from A. MySQL's default utf8mb4_unicode_ci collation treats them as equal, so WHERE public_id = 'aB3' may match Ab3. Use a _bin collation or a BINARY column, and remember Laravel's where() inherits the column collation.encode(123) treats the input as a number only if you pass an int and use an integer-capable encoder; passing a numeric string like "00123" can be interpreted as binary data. Be explicit — cast to int for IDs, pass raw bytes for binary payloads. Leading zero bytes are the classic failure: binary encoding preserves them, integer encoding does not, so round-tripping "\x00\x01" as binary is safe but converting via integer is not.gmp is fastest and handles big values; bcmath is a fallback; the pure-PHP/integer encoders are limited by PHP_INT_MAX (~9.2e18) and will break on large byte strings. Pick one and stick to it — encoded strings are not interchangeable across encoders.int, decode() gives you a numeric string. Cast before using in arithmetic or whereIn on integer columns, or Eloquent will happily compare against a string key.orderBy on an encoded column will look scrambled. Sort by the underlying integer.php:fpm) it silently falls back, which can change encoded output for the same input. Explicitly set encoder to remove the ambiguity, and confirm ext-gmp is installed in composer.json's require/suggest and your Dockerfile.decode(encode($x)) !== $x, the usual culprits are: mismatched alphabets between encode/decode instances, a value that exceeded the integer backend's range, or URL/browser normalization of the code (don't strip or urlencode it — Base62 is already URL-safe). Write a quick round-trip test with your exact encoder and alphabet; it catches most issues in seconds.Base62 or wrap it in your own ShortId service if you need validation, length checks, or fixed alphabets — the base class has no validation hooks of its own.How can I help you explore Laravel packages today?