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

Laravel Bcmath Cast Laravel Package

yuwuhsien/laravel-bcmath-cast

Eloquent casts for PHP 8.4's BCMath\Number object API. Turn model attributes into arbitrary-precision decimals and do natural operator-based arithmetic—add, subtract, multiply, divide—without bcadd/bcmul boilerplate. Requires PHP 8.4+ and Laravel 12+.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm the two hard gates before anything else. The package requires PHP 8.4+ (for the native BCMath\Number class) and Laravel 12+ — Laravel 11 support was dropped in v1.2.2 with no shim. There is no degraded mode; Composer simply refuses to install. Check php -v and composer show laravel/framework in every app, worker, and reporting service that will hydrate the affected models.

composer require yuwuhsien/laravel-bcmath-cast

Read the one class first. The entire runtime surface is a single Eloquent cast: vendor/yuwuhsien/laravel-bcmath-cast/src/Casts/AsDecimal.php. There is no service provider, no config file, no migration, no container binding. Five minutes with that file tells you everything the package does — worth doing before you trust it with money.

Fix the schema before wiring the cast. The cast provides zero correctness benefit on FLOAT/DOUBLE; it only preserves precision that the column already stores. Convert to DECIMAL with explicit precision and scale first:

$table->decimal('price', 10, 2);     // ✅
$table->decimal('tax_rate', 8, 6);   // ✅
$table->float('amount');             // ❌ defeats the purpose

Pilot on one attribute. Pick a low-risk, high-visibility field (catalog price), declare the cast, and exercise it in Tinker:

use YuWuHsien\Decimal\Casts\AsDecimal;

protected function casts(): array
{
    return ['price' => AsDecimal::class, 'cost' => AsDecimal::class];
}
$product = Product::create(['price' => '19.99', 'cost' => '12.50']);

$product->price instanceof \BCMath\Number;      // true
$margin = $product->price - $product->cost;      // Number('7.49')
(string) $margin;                                // "7.49"

That round trip — write a string, read a Number, compute, stringify — is your acceptance smoke test. Everything else is scale.

Implementation Patterns

Always write strings, never floats. The cast accepts Number, numeric strings, and integers. Floats are accepted but emit a warning — and a warning is not a guardrail. Enforce at the request boundary, not in the cast:

'price' => ['required', 'decimal:0,4', 'max:999999.9999'],

Then assign (string) $request->validated('price') or the raw validated string. If you accept JSON payloads, cast the value to string at the edge rather than letting 29.99 become a float mid-flight.

Keep arithmetic in PHP, keep comparison in SQL. Use AsDecimal for computation; use plain string bindings for queries. Query builder calls bypass casts entirely:

Product::where('price', '>', (string) $threshold)->get();   // stringify
Product::whereBetween('price', ['10.00', '20.00']);         // strings
Product::query()->update(['price' => (string) $newPrice]);  // bypasses the cast

Sorting, filtering, and pagination belong in SQL. Only reduce over Number objects when you genuinely need PHP-side exactness.

Define a rounding policy per field and apply it before persisting. The cast yields a value; it does not impose a scale or rounding mode. Intermediate division can carry more digits than your DECIMAL(10,2) column, and the database will silently round (or error, under strict mode) on write:

$total = ($product->price * $qty) * (new Number('1') + $product->tax_rate);

$product->total = $total->round(2, \BCMath\RoundingMode::HalfEven);
$product->save();

Write that policy down once, in one place, and reuse it — don't let each call site invent its own.

Type-hint BCMath\Number, not vendor classes. The hydrated value is a PHP language built-in. Domain services that receive it depend on the runtime, not on yuwuhsien/*. That is the package's best property: if upstream is abandoned, you reimplement one cast class and nothing else changes.

final class OrderTotal
{
    public function __construct(private Number $subtotal, private Number $taxRate) {}

    public function gross(): Number
    {
        return $this->subtotal * (new Number('1') + $this->taxRate);
    }
}

Test with string equality, not object equality. Number instances are values, not identities:

expect((string) $product->price)->toBe('19.99');
expect($margin->compare(new Number('7.49')))->toBe(0);

Build a round-trip suite per field and treat it as an acceptance gate: write '0.00' → read back; write null → read back; write ''; write a value exceeding column precision and assert it fails loudly rather than truncating. v1.2.2 shipped a fix for exactly this class of defect ("align set() input validation with what get() can parse back"), so assume field semantics are pre-stable relative to your edge cases.

Shape the API boundary deliberately. Decimal attributes serialize as JSON strings — semantically correct, and a breaking change for any client with numeric-typed fields. Decide per resource:

return [
    'price'        => (string) $this->price,                                  // raw precision
    'price_display'=> number_format((float) (string) $this->price, 2),        // presentation only
    'currency'     => $this->currency,                                        // keep separate
];

Treat that as an API versioning decision, not a side effect of a Composer update.

Gotchas and Tips

  • Objects are immutable. $product->price->add(new Number('1')) does nothing; reassign. And $product->price === $product->price is false — a fresh instance is built on each attribute read, so cache it in a local when used in loops.
  • Object bindings are unreliable. Some paths stringify Number implicitly, others don't. Never pass a Number into where(), update(), insert(), or whereIn() — always (string) $n.
  • null and '' are separate cases. Confirm your version's behavior for both before relying on either; the v1.2.2 changelog is a hint that this boundary has moved.
  • Division by zero throws. Guard denominators in margin/ratio calculations — DivisionByZeroError, not a silent INF.
  • Aggregates don't come back as Number. Product::sum('price') returns whatever the driver hands back (usually a string). Pick SQL or PHP as the source of truth for totals and verify parity on representative data; divergence surfaces in reconciliation, not in tests.
  • Dirty tracking still uses raw strings. getRawOriginal('price') and getChanges() return strings — which is good news for audit logs. Just don't assume isDirty() compares Number objects.
  • Queue and cache payloads. Serialize decimals as strings when passing through jobs, events, or cache; don't rely on Number's serialize hooks surviving every driver and worker version.
  • No config means no per-field scale shortcut. AsDecimal::class.':2' parameter syntax isn't documented — verify before designing around it, or write a thin cast that composes Number and applies your rounding policy. That's the only sanctioned extension point: subclass or compose the cast.
  • Vendor risk is real but bounded. Two stars, one maintainer, zero dependents, no security policy. Mitigate by pinning the version and keeping your own round-trip test suite as the gate — not by adding an abstraction layer. With a one-class surface and a native hydrated type, the exit cost is a single pull request.
  • Debugging one-liner: dd($model->price::class, (string) $model->price, $model->getRawOriginal('price')); — distinguishes "cast returned a Number" from "the string that actually hit the column," which is where precision bugs live.
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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr