Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Bcmath Polyfill Laravel Package

nanasess/bcmath-polyfill

Pure-PHP polyfill for PHP's bcmath extension, providing bcadd, bcmul and the rest of the arbitrary-precision math functions when the native extension isn't installed. Drop-in compatibility for libraries and apps that rely on bcmath, with no server configuration changes needed.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require nanasess/bcmath-polyfill

That's the whole install. There is no service provider, no config file, no facade — it's a Composer autoload.files entry that conditionally declares the global bc* functions only if they don't already exist. If ext-bcmath is loaded, it is a no-op.

Look here first:

  1. vendor/nanasess/bcmath-polyfill/composer.json — check the autoload.files entry and the phpseclib/phpseclib dependency (this is a fork of phpseclib/bcmath_compat, so all math is delegated to phpseclib3\Math\BigInteger). If you already ship phpseclib, you're paying almost nothing extra.
  2. vendor/nanasess/bcmath-polyfill/src/ — confirm which functions are actually shimmed. The README lists core ones (bcadd, bcsub, bcmul, bcdiv, bcmod, bcpow, bcsqrt, bccomp, bcscale) plus the PHP 8.4 additions (bcceil, bcfloor, bcround, bcdivmod). Verify the PHP 8.4 list against the source before you depend on it — that's the entire reason to pick this fork over phpseclib/bcmath_compat.
  3. The function_exists() guards — they're what makes it safe in CI images that do have the extension.

Verify it's live in a container without the extension:

docker run --rm -v "$PWD":/app -w /app php:8.4-cli \
  php -r "var_dump(extension_loaded('bcmath'), function_exists('bcadd'), bcadd('0.1','0.2',2));"
# bool(false) bool(true) string(3) "0.30"

First real use case: a Laravel app on shared hosting (or a slim Alpine/FrankenPHP image) running a money/cart package that hard-requires BCMath. Install the polyfill, and the package's bcadd() calls start working with zero code changes.

Implementation Patterns

Put it in require, never require-dev. It's a runtime shim. If a downstream package (e.g. moneyphp/money, brick/money, a payment SDK) lists ext-bcmath in its require, Composer will refuse to install. Resolve it in your app's composer.json:

{
  "provide": { "ext-bcmath": "8.4.0" },
  "require": { "nanasess/bcmath-polyfill": "^1.0" }
}

provide tells Composer the platform requirement is satisfied. Do this only after the polyfill is in place, and know that you're asserting a capability — if you later drop the polyfill, installs break loudly (which is what you want).

Prefer explicit scale over bcscale(). bcscale() mutates process-global state that survives across a whole request (and across queued jobs in a long-running worker). Any library that calls bcscale() on boot will silently change the precision of everything else. Set scale per call:

bcadd($a, $b, 2);

If you must use a global default — because a third-party package relies on it — set it in a service provider's register() so it happens before any math runs, and never touch it again:

public function register(): void
{
    bcscale(2);
}

Wrap it behind a value object so the polyfill stays an implementation detail and you can unit-test without the global functions:

final readonly class Money
{
    private function __construct(public string $amount, public string $currency) {}

    public static function of(string $amount, string $currency): self
    {
        return new self(bcadd($amount, '0', 2), $currency);
    }

    public function add(self $other): self
    {
        if ($other->currency !== $this->currency) {
            throw new InvalidArgumentException('Currency mismatch');
        }
        return new self(bcadd($this->amount, $other->amount, 2), $this->currency);
    }

    public function multiply(string $factor): self
    {
        return new self(bcmul($this->amount, $factor, 2), $this->currency);
    }
}

Note bcadd($amount, '0', 2) — that's the canonical "normalise to 2dp" trick and it works identically on both implementations.

Use bcdivmod() sparingly and behind your own wrapper. It's PHP 8.4-only natively; on PHP 8.2 you'll be calling the polyfill's emulation. Isolate it in one method so you can swap it out:

private function divmod(string $a, string $b, int $scale = 0): array
{
    if (function_exists('bcdivmod')) {
        return bcdivmod($a, $b, $scale); // [quotient, remainder]
    }
    return [bcdiv($a, $b, 0), bcmod($a, $b, $scale)];
}

Testing pattern — run the suite twice. The polyfill is invisible on your laptop because you have ext-bcmath. Gate it in CI:

- name: Tests (native bcmath)
  run: php artisan test

- name: Tests (polyfill only)
  run: docker run --rm -v "$PWD":/app -w /app php:8.4-cli vendor/bin/phpunit

The official php:*-cli images do not compile BCMath by default (that's why docker-php-ext-install bcmath is such a common Dockerfile line), which makes them a clean, reproducible polyfill-only environment.

Laravel-specific integration notes. Laravel core doesn't require BCMath, so adding this is purely for a domain package. It composes fine with Illuminate\Support\Number / NumberFormatter — those use intl, not BCMath, so don't expect them to be affected. Where it matters: money/currency packages, tax engines, currency conversion, and phpseclib RSA operations on hosts lacking gmp.

Gotchas and Tips

  • Performance is the headline risk. This is pure-PHP big-integer math on top of phpseclib. Expect it to be one to two orders of magnitude slower than the C extension. Fine for a checkout total; not fine for a 100k-row report loop. Profile before you blame your Eloquent query.
  • Global-namespace function resolution. In a namespaced file, an unqualified bcadd() call falls back to the global function at runtime — fine, but it defeats opcache function-call optimisation and confuses static analysers. Prefer use function bcadd; at the top of each file, which also makes the dependency greppable.
  • IDE / PHPStan won't see the functions. The polyfill declares them inside conditionals in an autoloaded file; static analysis tools generally can't follow that. Add php-stubs/bcmath (or the ext-bcmath stubs bundled with PHPStan) to require-dev so you get signatures and don't chase phantom "undefined function" errors.
  • PHP 8.4 rounding modes are the sharp edge. PHP 8.4 added bcceil, bcfloor, bcround, bcdivmod, and bcround takes a RoundingMode enum. On PHP 8.2/8.3 there is no RoundingMode enum in core, so any polyfill signature for it has to invent one or accept an int. Read the source for the exact signature before you typehint \RoundingMode — this is where a fork can diverge from native behaviour, and it's the most likely place for a silent bug in your tax rounding.
  • Exception semantics drifted between PHP versions. Native BCMath changed over time: bcdiv() by zero went from a warning + null/false to throwing DivisionByZeroError, and invalid scale values moved to ValueError. A compat layer can't be byte-identical to every PHP minor. Don't write try/catch (DivisionByZeroError) and assume symmetry across PHP 8.1 → 8.4; test both paths.
  • Never pass floats. bcadd(0.1, 0.2) has already lost precision before BCMath sees it. Always cast at the boundary: bcadd((string) $input, '0', 2). Validate with is_numeric() and reject exponent notation (1.5e3) — BCMath won't parse it.
  • bcscale() is sticky process state. Under Octane, FrankenPHP worker mode, or a queue worker, a scale set by one job leaks into the next. If a package calls bcscale(), re-assert your value at the start of each job or, better, never rely on the default.
  • bcmod() on negatives and non-integers is a classic divergence. phpseclib's implementation and the C extension have historically differed on sign handling. If you do modulus math on user input, add an explicit test matrix with negative operands.
  • Don't use it for crypto secrets or constant-time comparisons. bccomp() on this path is not constant-time, and the underlying BigInteger isn't hardened. For RSA/ECDSA, depend on phpseclib directly (which you get transitively) or require ext-gmp.
  • No extension points — wrap instead. There's nothing to hook, configure, or swap. If you need to test failure paths (division by zero, overflow), you cannot monkey-patch global functions in PHP. The wrapper class from the patterns section is your seam; bind it in the container and inject a fake.
  • Version-pin deliberately. Stars are low (2), releases are infrequent, and the PHP 8.4 surface is still moving upstream. Pin a caret range, and after every composer update re-run the polyfill-only CI job — a silent behaviour change in a rounding function is exactly the kind of bug that ships to production undetected.
  • Debugging checklist: extension_loaded('bcmath') tells you which path is live; function_exists('bcround') tells you whether the 8.4 shims registered; if the latter is false on a PHP 8.4 host without the extension, the polyfill's autoload entry didn't run (usually a composer dump-autoload or a --no-scripts install).
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource