stephenhill/base58
Base58 encoding and decoding for PHP with support for the Bitcoin and Flickr alphabets, arbitrary-precision math via GMP or BCMath, and a Laravel service provider for quick integration into your application.
Install the library with Composer:
composer require stephenhill/base58
Check which math backend you have available. The library can use GMP, BCMath, or a pure-PHP fallback, but GMP is significantly faster:
php -m | grep -E 'gmp|bcmath'
Minimal usage:
use StephenHill\Base58;
$base58 = new Base58(); // default Bitcoin alphabet, auto-selects GMP/BCMath/native math
$encoded = $base58->encode('Hello World!');
$decoded = $base58->decode($encoded);
var_dump($encoded); // "2NEpo7TZRRrLZSi2U"
var_dump($decoded); // "Hello World!"
As of v2.1.0 there are two static constructors, which remove most of the manual wiring:
use StephenHill\Base58;
$base58 = Base58::create(); // new instance (accepts the same args as the constructor)
$shared = Base58::singleton(); // one shared instance per process
The default alphabet is the Bitcoin alphabet:
123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
In a Laravel app, bind a configured singleton in AppServiceProvider. With v2.1.0 this is a one-liner if you are happy with the defaults:
use StephenHill\Base58;
public function register(): void
{
$this->app->singleton(Base58::class, fn () => Base58::singleton());
}
If you need a configured alphabet or a forced adapter, keep the explicit construction (see below) and wrap it in create():
use StephenHill\Base58;
use StephenHill\BCMathAdapter;
use StephenHill\GMPMathAdapter;
use StephenHill\NativeMathAdapter;
public function register(): void
{
$this->app->singleton(Base58::class, function () {
$adapter = extension_loaded('gmp')
? new GMPMathAdapter()
: (extension_loaded('bcmath')
? new BCMathAdapter()
: new NativeMathAdapter());
return Base58::create(config('base58.alphabet'), $adapter);
});
}
Where to look first: the StephenHill\Base58 class itself is small; the important extension points are the constructor alphabet argument, the new create()/singleton() static helpers, and the MathAdapterInterface implementations. The test suite is the fastest place to find known-answer vectors for encode/decode behavior.
First useful day-to-day use case: turning a UUID or 16 random bytes into a shorter, copy-paste-friendly public identifier:
$publicId = app(Base58::class)->encode(random_bytes(16)); // roughly 21–22 characters
Configuration and alphabet selection. Keep the alphabet in config so every service uses the same one:
// config/base58.php
return [
'alphabet' => env(
'BASE58_ALPHABET',
'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
),
];
Use one alphabet per identifier namespace. If you use the Flickr alphabet somewhere, treat those IDs as a different type from Bitcoin-alphabet IDs.
$flickr = Base58::create('123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ');
Choose create() vs singleton() deliberately. Base58::create() gives you an isolated instance — use it in tests, jobs, and anywhere you want a specific alphabet. Base58::singleton() gives you one shared, already-configured instance for the whole process; it is the right default for a stateless codec that only ever uses one alphabet. Do not mix the two for different alphabets in the same process.
Encode and decode UUIDs. Pack the canonical UUID hex into 16 bytes, then encode. Keep the byte order consistent.
function encodeUuid(Base58 $base58, string $uuid): string
{
return $base58->encode(hex2bin(str_replace('-', '', $uuid)));
}
function decodeUuid(Base58 $base58, string $encoded): string
{
$hex = bin2hex($base58->decode($encoded));
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split($hex, 4));
}
Validate user input. Do not use a loose regex if you can call decode() and catch the library exception:
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use StephenHill\Base58;
class Base58String implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (!is_string($value) || $value === '') {
$fail('The :attribute must be a non-empty Base58 string.');
return;
}
try {
app(Base58::class)->decode($value);
} catch (\InvalidArgumentException) {
$fail('The :attribute is not valid Base58.');
}
}
}
Persist encoded IDs deliberately. If the database column stores the Base58 string, no cast is needed; encode at the service/API boundary. If the column stores raw bytes and you want Base58 in and out of Eloquent, a custom cast is reasonable:
public function set($model, $key, $value, $attributes): array
{
return [$key => $value === null ? null : app(Base58::class)->encode($value)];
}
public function get($model, $key, $value, $attributes): ?string
{
return $value === null ? null : app(Base58::class)->decode($value);
}
Route model binding. If the model stores an encoded public_id, return that column from getRouteKeyName(). If it stores raw binary, override resolveRouteBinding() to decode the route parameter first.
Base58Check if you need Bitcoin compatibility. This package performs raw Base58 only. Bitcoin addresses require a version byte plus a 4-byte checksum:
$payload = "\x00" . $hash160;
$checksum = substr(hash('sha256', hash('sha256', $payload, true), true), 0, 4);
$address = $base58->encode($payload . $checksum);
Adapter choice. Force the GMP adapter in production when available. The native adapter is fine for occasional short IDs but will be noticeably slower for bulk work. Do not instantiate GMPMathAdapter on a host without ext-gmp. Note that singleton() and a bare create() will auto-select; only pass an adapter explicitly when you need to pin the backend.
API resources. Encode once at the boundary and expose a stable string in JSON resources. Avoid making every controller remember the alphabet.
utf8mb4_unicode_ci collation is case-insensitive, which can produce wrong lookups and uniqueness conflicts. Store IDs in a case-sensitive column (utf8mb4_bin, VARBINARY, or equivalent) or compare with binary semantics.decode() throws InvalidArgumentException for characters outside the alphabet. Wrap untrusted input in try/catch; otherwise malformed URL segments become 500 errors.decode() returns raw binary. var_dump() output is not useful; use bin2hex(), unpack(), or compare against expected byte strings.\x00 to a leading 1; include a known vector in your tests if you persist fixed-width values.Base58::singleton() is process-wide, not container-scoped. That is fine because the object is stateless, but it bypasses Laravel’s container: tests that rebind Base58::class will not affect code that calls Base58::singleton() directly, and a multi-tenant app that varies the alphabet per tenant should not use it. Prefer the container binding (app(Base58::class)) inside application code and reserve the static helpers for the service provider.Singleton() state persists across Octane requests and long-running queue workers. The codec carries no request data, so this is safe, but do not add mutable per-request state to anything you obtain from it.create() signature has evolved (v2.1.0 added the statics). Check the released class signature before assuming argument order for alphabet and adapter, and pin a minor version.random_bytes(), not uniqid() or rand(). Base58 changes representation only; it adds no entropy.How can I help you explore Laravel packages today?