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.
Architecture fit. Base58 is a narrow, stateless primitive — encode/decode of arbitrary byte strings into a human-resistant alphabet (no 0OIl). It is not an application framework concern, so it should sit at the edge of the domain: value objects (e.g., Base58Id), API resource transformers, or a small Base58Codec service bound in the container. It does not compete with Laravel's existing primitives (Str, Hash, Encrypter) and should never be used for anything security-sensitive beyond representation. Fit is good only if the product genuinely requires Base58 (Bitcoin/Solana-style addresses, IPFS CIDv0, compact public identifiers). If the requirement is merely "shorter, URL-safe IDs," Base62/Base64url or symfony/uid avoids adding a dependency entirely.
v2.1.0 adds Base58::create() and Base58::singleton(), which nudges the package from a pure static codec toward a factory/instance model. That is architecturally fine — instances are actually easier to inject and fake than statics — but the singleton() accessor introduces process-global state into a primitive we previously characterized as stateless. Treat the instance API as the supported path and the singleton as a convenience you deliberately do not adopt in domain code.
Integration feasibility. Still low friction: composer require stephenhill/base58, no service provider, no config publish, no migrations. The library is a codec, not a subsystem. The new statics do not change the install story, but they do give you a choice of three entry points (new Base58(...) if the constructor is public, Base58::create(), Base58::singleton()), and picking the wrong one for the framework is the only new decision. The real feasibility question remains the arbitrary-precision backend — most Base58 implementations in PHP depend on ext-gmp or ext-bcmath. That is an infrastructure decision, not a code decision, and it must be confirmed before the sprint starts.
Technical risk. Moderate, driven by ecosystem signal rather than code complexity:
Base58::singleton() implies a statically-held instance. In a long-lived worker (Octane, Swoole, queue daemons) that instance survives across requests; if it is configurable or mutable, state can leak between tenants/jobs. In PHPUnit it creates test-order dependence unless there is a reset hook. Neither is fatal, but both must be confirmed.1 characters) is the single most common correctness bug in these libraries. Verify with round-trip property tests.Key questions.
create() accept alphabet/backend arguments, and does that make the singleton parameterized?ext-gmp/ext-bcmath in composer.json (soft vs hard requirement)?create() vs singleton() — is the singleton lazily constructed, is it keyed by configuration, and is there a documented reset/clear() method for tests and worker boundaries?create() cheap enough to call per-encode, or does it re-allocate big-number state each time (relevant if it lands in a hot path)?Stack fit. Composer-native, PSR-4, no framework coupling — it drops into Laravel 9/10/11/12 without conflict. It does not touch the container, HTTP kernel, queue, or Eloquent. The correct pattern is a thin first-party adapter:
App\Support\Base58\Base58Codec (interface) with encode(string): string and decode(string): string.StephenHillBase58Codec implementing it, holding a single injected instance obtained once from Base58::create() in the service provider — instance-per-binding, not singleton(). Mark the vendor class @internal and confine it to one file.tuupola/base58, bitcoin-php, or a brick/math-based in-house implementation.Base58::singleton() from application code via an architecture test or code-review rule. If a teammate uses it, the adapter boundary is already bypassed and the swap-out guarantee is void.Migration path. Greenfield: trivial. Brownfield: the migration risk is data, not code. Any existing identifiers stored in Base58 must round-trip identically under the new library — canonically, byte-order and leading-zero behavior must match the previous encoder exactly. Plan a dual-read verification window: encode/decode with both implementations against a production sample and diff. If Base58 appears in URLs or external contracts, treat the alphabet and canonical form as a frozen interface. Since v2.1.0 is additive, upgrading from 2.0.x should be a no-op behaviorally — but assert that with the existing fixture corpus rather than assuming.
Compatibility. Verify against the project's PHP matrix. If GMP is required, confirm the production image, the CI image, and any local docker-compose all enable it — a composer install that succeeds but a runtime that throws Class 'GMP' not found is the predictable failure. Also check for conflicts with other packages that polyfill or wrap GMP. Note that the singleton pattern interacts with Octane's long-lived workers: if adopted, it must be reset on worker boot or it will pin a stale instance across deploys.
Sequencing.
singleton() semantics before the adapter is written. Pin a specific minor in composer.json.Base58::singleton is unreferenced outside the adapter.Alternatives to weigh in the same decision record: tuupola/base58 (materially more adoption), symfony/uid if the actual need is short unique IDs, or ~40 lines of brick/math if you want zero new dependencies and full control. v2.1.0 slightly narrows this gap by improving DI ergonomics, but it does not change the ecosystem signal.
Maintenance. Very low ongoing cost — it is a pure function with no I/O, no config, no migrations. Two caveats. First, singleton() means the package now can hold state; if anyone calls it, "no global state" stops being a property of the system and becomes a convention you enforce. Second, the maintenance burden is otherwise almost entirely supply chain: tracking releases, watching for abandonment, and owning a fork if necessary. Given 0 dependents and modest stars, assume you may eventually vendor the code into app/Support rather than depend on upstream. That is a ~1-hour exit and should be documented as the contingency. Pin to an exact version in composer.lock and require deliberate upgrades; do not use a floating constraint on a low-signal package.
Support. No commercial support, no SLA, no security disclosure process to rely on. Internal support is trivial because the surface is two methods and the code is auditable. The genuine support cost is knowledge: one engineer should understand Base58's leading-zero and endianness semantics well enough to debug a mismatch, because "the ID decodes differently" is an opaque failure to everyone else on the team. The static factories also create a support question the docs should pre-empt — "which of the three ways do I construct this?" — answered once in the design note.
Scaling. CPU-bound and linear-ish in input size with big-integer division dominating — expect Base58 to be meaningfully slower than Base64 (roughly an order of magnitude for large inputs). For 16–32 byte identifiers this is microseconds and irrelevant. For blobs, it is the wrong tool: do not Base58-encode file contents or large JSON. If encoding appears in a hot request path, benchmark under realistic concurrency and consider caching encoded forms. If create() performs non-trivial initialization, do not call it per-encode — resolve it once via the container. Memory is transient and proportional to input; watch for very large strings passed accidentally from user input.
Failure modes.
Error at first use, not at boot. Mitigate with a boot-time capability check or a health check.1s, whitespace) → decodes to the same bytes but re-encodes differently, breaking equality checks and idempotency. Normalize on ingest.singleton() in application code.Ramp-up. Effectively zero for any PHP developer: two methods, familiar Composer install. The learning curve is conceptual, not API — engineers must internalize that Base58 is an encoding, not encryption or a hash, and must not treat encoded values as secrets or as collision-resistant identifiers. v2.1.0 adds one more conceptual footnote: prefer injected instances over static access, for the same reason you prefer injected services over facades in domain code. Budget a short design note in the repo covering the alphabet in use, canonicalization rules, the sanctioned constructor path, and the swap-out procedure. Overall: a low-risk, low-cost dependency whose only real hazards are that "low cost" tempts teams to adopt it without confirming that Base58 — rather than Base62 or a ULID — is the actual requirement, and that the new singleton accessor quietly reintroduces global state into a primitive that did not have any.
How can I help you explore Laravel packages today?