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

Money Laravel Package

brick/money

Brick\Money is a PHP library offering immutable Money and Currency classes with exact arithmetic and explicit rounding control, avoiding floating-point errors. Built on brick/math, it handles any amount size. Requires PHP 8.2+; GMP or BCMath recommended for speed.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin. composer require brick/money pulls in brick/math. Confirm your PHP version first: the current cycle requires PHP 8.2+, which means Laravel 11/12 are fine out of the box, while Laravel 10 projects on PHP 8.1 need the older 0.10 line. Because every breaking change starts a new 0.x cycle, pin an explicit cycle ("brick/money": "0.11.*") rather than ^0.11 blind-trusting future majors. Also install ext-gmp or ext-bcmath — the library works without them but falls back to slower pure-PHP arithmetic.

First use case: replace float money in one path. The highest-value entry point is your order/invoice total calculation. Instead of $total = $price * $qty * 1.2, do:

use Brick\Money\Money;
use Brick\Math\RoundingMode;

$price = Money::of('19.99', 'EUR');          // EUR 19.99
$line  = $price->multipliedBy(3);            // exact, no float drift
$withTax = $line->multipliedBy('1.20', RoundingMode::HalfUp);

Where to look first, in order: the README's Creating a Money → Basic operations → Money contexts sections, then Brick\Math\RoundingMode (the enum you will type constantly), then Advanced calculations (RationalMoney) and Money allocation. Read the CHANGELOG once before upgrading — in a 0.x library, minor bumps on the x position are breaking changes.

Implementation Patterns

Persist minor units, not decimal strings. The durable pattern is two columns: amount_minor BIGINT and currency CHAR(3). Hydrate with Money::ofMinor($row->amount_minor, $row->currency) and dehydrate with $money->getMinorAmount()->toInt(). This sidesteps SQL DECIMAL scale drift, makes indexes and SUM() aggregates cheap, and keeps the currency's own scale as the single source of truth (JPY has 0 decimals, BHD has 3 — never hardcode 2).

Wrap it in an Eloquent cast so domain code never touches primitives:

class MoneyCast implements CastsAttributes
{
    public function get($model, $key, $value, $attributes): ?Money
    {
        return $value === null ? null : Money::ofMinor($value, $attributes['currency']);
    }

    public function set($model, $key, $value, $attributes): array
    {
        return [$key => $value?->getMinorAmount()->toInt()];
    }
}

Guard against a null currency, and add a currency cast/observer so the pair can never diverge.

Use RationalMoney for multi-step math, Money at the boundaries. Any chain of tax, discount, and proration steps should run on fractions and round exactly once:

$net = $gross->toRational()
    ->dividedBy(1.20)
    ->minus($discount->toRational())
    ->toContext($gross->getContext(), RoundingMode::HalfUp);

Rounding at every intermediate step is the classic source of cent-level reconciliation bugs; toContext() at the end is the fix.

Use allocate() for anything that must sum back to the original. Splitting a captured payment, distributing a refund fee, or dividing a shared invoice: $money->allocate([48, 41, 11], AllocationMode::FloorToFirst) guarantees the parts add up to the whole (with the remainder handed to the first allocatees or returned as a separate element). Use split() for even n-way splits. Ratios need not total 100 and can be decimals or fractions.

Keep currency conversion at the edge. Inject an ExchangeRateProvider (the ConfigurableProvider for a rate table you manage, or a PDO-backed one) and pass it to $money->to('EUR', $provider, RoundingMode::HalfUp). Persist the rate and rate date alongside the converted amount — an auditable conversion is worth more than a recomputable one. For reports spanning currencies, MoneyBag lets you sum mixed currencies and inspect per-currency totals rather than throwing CurrencyMismatchException.

Format at the presentation layer, not in the model. IntlMoneyFormatter (needs ext-intl, universally present in Laravel images) gives locale-aware €1.234,56 output; DecimalMoneyFormatter gives a stable machine string for exports, CSVs, and Money::of() round-trips. In API resources, emit {amount_minor, currency, formatted} and let the front end stay in integer arithmetic — never hand a float over the wire.

Test the arithmetic, not the strings. Assert with isSameValueAs() for currency-and-amount equality and isEqualTo()/compareTo() when a currency mismatch should fail loudly. Build a small set of golden cases (rounding boundary at .005, a JPY zero-scale case, a 3-decimal currency) — those catch 90% of regressions.

Gotchas and Tips

RoundingNecessaryException is a feature, not a nuisance. Any operation whose result doesn't fit the context's scale throws until you supply a RoundingMode. Decide the policy per domain, not per call site: e.g. line items Down, tax HalfUp, cash payments HalfEven. Scattering ad-hoc modes is how totals start disagreeing between the cart and the invoice.

The rounding mode is not stored. Money::of('123.456', 'USD', roundingMode: RoundingMode::Up) uses the mode once, then forgets it; every later plus/dividedBy needs its own. There is no "configure the object's rounding" escape hatch — this trips people up coming from moneyphp/money-style APIs.

Contexts are invisible in the database. DefaultContext (ISO scale), CashContext(step: 5) for CHF cash, CustomContext(scale: 4) for unit prices, and AutoContext all produce Money objects that look identical in a BIGINT column. Persist the scale (or the context kind) if you ever need to rebuild the exact object, and use one context consistently per column. Mixing contexts in arithmetic silently adopts the receiver's context, which produces plausible-looking but wrong results.

AutoContext will bite you on division. It cannot represent infinite repeating decimals, so dividedBy(3) on AutoContext throws. Reserve it for one-off display values, and reach for RationalMoney whenever a division is involved.

Currencies change. The minor unit of a currency can be redefined in ISO 4217, and the library explicitly reserves the right to change alpha-code lookups in a breaking release. Don't cache a scale in your own schema, don't assume Currency::of('XYZ')->getDefaultFractionDigits() is immutable across upgrades, and re-run your currency-sensitive tests on every dependency bump.

Floats are rejected. Money::of(19.99, 'USD') with a literal float is a bug waiting to happen — always pass ints or strings ('19.99', '1.20'). The same applies to ratios and multipliers in multiplyBy/allocate. A cast like (string) $request->input('amount') at the request boundary is cheap insurance.

Don't serialize Money for storage. Its string form is display-oriented and not a safe round-trip, and Eloquent's serialize()/JSON paths will not preserve the context. Persist minor + currency (or a DecimalMoneyFormatter string) and rehydrate.

Comparing across currencies throws. plus(), isEqualTo(), and friends all raise CurrencyMismatchException on differing currencies. Use isSameValueAs() when you want a false instead of an exception (e.g. filtering a mixed list), and MoneyBag when you need real cross-currency aggregation.

Extension points are genuinely open. Implement ExchangeRateProvider for your bank feed, a custom Context for loyalty points or a 4-decimal unit price, a custom CurrencyProvider if you need crypto or internal currencies alongside ISO ones, or a MoneyFormatter for a house invoice style. Prefer these over subclassing Money — it is a final-ish immutable value type and the library composes better than it inherits.

Keep it in the domain layer, not controllers. The biggest day-to-day win isn't the API surface; it's that every controller, job, and Artisan command stops doing money arithmetic and delegates to a handful of well-named domain methods that return Money. That's the refactor worth planning for on day one.

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.
thuraaung2493/laravel-rulecraft
cyberwizard/schedule-catchup
livetennisapi/livetennisapi-laravel
roundly-consulting/notifications-for-laravel
flightsadmin/bootstrap-starter-kit
lasserafn/laravel-intempus
likyly/sdk
tdt/input
coreation/php-shapefile
coreation/gisconverter
primix/widgets
primix/notifications
primix/actions
primix/tables
primix/details
primix/forms
primix/support
marcelgwerder/laravel-api-handler
firebrikdotcom/conveyor-laravel-broadcaster
kanata-php/conveyor-laravel-broadcaster