Library / SDK
brick/money avatar
brick/money

brick/money makes rounding an explicit decision in PHP

A money and currency library for PHP

1,938 stars121 forksPHPMIT

At a glance

What is it?
brick/money is an MIT licensed PHP library for immutable money and currency values, built on brick/math, where every operation that needs rounding demands a RoundingMode argument and throws otherwise. It requires PHP 8.2 or later and is versioned on a 0.x cycle that resets on breaking changes.
Who is it for?
brick/money fits a PHP application that stores or computes prices in more than one currency and wants the rounding decision to appear at the call site rather than inside a float, since the immutability, the currency mismatch exception and the RoundingNecessaryException are all enforced by the type system.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 32 days ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • brick/money1,938 ★ · +8
Star history
Repository2026-10-172026-11-07
brick/money1.9k1.9k

8 Stars in the last 21 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

Rounding is a parameter, not a silent side effect

The introduction states the premise plainly: the library provides immutable classes to work with monies and currencies, with exact arithmetic and explicit control over rounding, avoiding the silent rounding errors inherent to floating point. The mechanism that enforces this is unusual enough to be worth reading twice.

Any operation whose result cannot be represented exactly in the currency's smallest unit requires a RoundingMode. If you do not pass one, a RoundingNecessaryException is thrown. There is no default rounding behaviour to fall back on.

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

$money = Money::of(50, 'USD');

$money->plus('0.999'); // RoundingNecessaryException
$money->plus('0.999', RoundingMode::Down); // USD 50.99

$money->minus('0.999'); // RoundingNecessaryException
$money->minus('0.999', RoundingMode::Up); // USD 49.01

The same rule applies to the factory. Money::of('123.456', 'USD') throws, because 123.456 does not fit in the two decimal places USD uses, and Money::of('123.456', 'USD', roundingMode: RoundingMode::Up) gives USD 123.46.

There is a subtlety that trips up most newcomers, and the README calls it out directly. The rounding mode is only used once, for the value provided in of(). It is not stored in the Money object, and any subsequent operation still needs a RoundingMode passed when necessary. So a Money is not a value that carries a rounding policy, it is an exact value that forces you to state a policy at every point where a policy is required. That is the whole design, and it is the reason the library can claim explicit control over rounding rather than merely offering configurable rounding.

Immutability makes plus and minus return new objects

Money is an immutable class. Its value never changes, so it can be safely passed around, and every operation returns a new instance.

php
use Brick\Money\Money;

$money = Money::of(50, 'USD');

echo $money->plus('4.99'); // USD 54.99
echo $money->minus(1); // USD 49.00
echo $money->multipliedBy('1.999'); // USD 99.95
echo $money->dividedBy(4); // USD 12.50

Notice that the operands are plain strings and numbers, not just Money instances. You can add a decimal string to a Money, and you can add one Money to another.

php
$cost = Money::of(25, 'USD');
$shipping = Money::of('4.99', 'USD');
$discount = Money::of('2.50', 'USD');

echo $cost->plus($shipping)->minus($discount); // USD 27.49

Mixing currencies is an error rather than a silent conversion.

php
$a = Money::of(1, 'USD');
$b = Money::of(1, 'EUR');

$a->plus($b); // CurrencyMismatchException

That is a design decision with real consequences. There is no exchange rate anywhere in this library, so a mismatch can only ever be a bug, and the library refuses to guess. The same rule applies to comparisons: compareTo(), isEqualTo(), isGreaterThan(), isGreaterThanOrEqualTo(), isLessThan() and isLessThanOrEqualTo() all accept either a number or a Money instance, and if the argument is a Money it must be of the same currency or an exception is thrown. For the one case where you want to compare without the throw, there is isSameValueAs() in place of isEqualTo().

Immutability also means Money is safe to hold in a session, cache or shared collection without defensive copying, which is the practical reason to prefer a value object over a float pair.

from minor units to avoid a second rounding decision

There is a second factory method, and it exists to sidestep the rounding question entirely. Money::ofMinor() takes an amount expressed in the currency's minor units, the cents.

php
use Brick\Money\Money;

$money = Money::ofMinor(1234, 'USD'); // USD 12.34

This matters for any system where the value arrives from a source that already counts in minor units, which is most payment gateways and most databases that store integer amounts. Using of() there would mean parsing a decimal string and then applying a rounding mode to it, which is a decision you would be making about data you did not create. ofMinor() makes that decision vanish.

The pairing with of() also tells you something about the intended usage. of() is for human-facing amounts where you know the currency and want to write 19.90 and mean it. ofMinor() is for machine-facing amounts where 1990 means something specific. A library that supports both is telling you to pick the one that matches the source of the number rather than converting everything to one convention.

What the library does not provide is currency conversion. There is no method in the documented API for turning USD into EUR, and the composition of the type, with a currency attached to an exact amount and a mismatch exception on cross-currency arithmetic, points the same way. If you need conversion you need a rate source and a policy about rounding the converted result, and that policy will land in your code rather than in the library.

The 0.x cycle resets on breaking changes, which is a policy you can rely on

The project status and release process section is unusually explicit, and it is a better guarantee than most libraries of this size offer.

The current releases are numbered 0.x.y. When a non-breaking change is introduced, such as adding new methods or optimising existing code, y is incremented. When a breaking change is introduced, a new 0.x version cycle is always started. The consequence the README draws is the useful part: it is therefore safe to lock your project to a given release cycle, such as 0.15.*.

That is a real promise rather than a hope. A library that resets the minor series on a breaking change can be pinned to a series and you know that a patch or minor release within that series will not break you. The current published versions are 0.15.0 on 2026-09-09, 0.15.1 on 2026-09-14 and 0.15.2 on 2026-09-29, and the last push to main was on 2026-10-06.

The project also describes itself as still under development but well tested and stable enough to use in production environments, which is a candid framing of a pre-1.0 library. The upgrade path when you do need to move to a newer cycle is documented as a matter of course: check the release history for the list of changes introduced by each further 0.x.0 version. The repository has a CHANGELOG.md at the top level, and a release history is published on GitHub.

One more guarantee worth noting is that the library says it is well tested, and the repository shows the tooling: phpunit.xml and phpunit-bootstrap.php for the test suite, a codecov.yml for coverage reporting, a phpstan.neon for static analysis, and a CI workflow badge. The tests/ and src/ directories are at the top level.

ISO 4217 data moves, and the project tells you which moves count as breaking

Currency information comes from the latest ISO 4217 standard, and the README describes that standard as a living one, so updates to currencies are expected to happen regularly. Regular currency updates would be disruptive if every change broke consumers, so the project draws a distinction.

Updates that will be considered breaking changes, and will only be released in a new major version after 1.0: currencies obtained by alpha currency code such as EUR or USD, through Currency::of(), Money::of(), Money::ofMinor(), IsoCurrencyProvider::getCurrency() and similar methods. In other words, if a currency's alpha code, its decimal places, or its existence changes, that is a breaking change and it waits for a major release.

Updates that are not considered breaking and may ship in minor releases after 1.0: currencies obtained by numeric currency code such as 978 or 840 through Currency::ofNumericCode() and IsoCurrencyProvider::getCurrencyByNumericCode(), current currencies obtained by country code such as FR or US through Currency::ofCountry() and the IsoCurrencyProvider country methods, and historical currencies obtained by country code through IsoCurrencyProvider::getHistoricalCurrenciesForCountry().

So a new numeric code or a shift in the country mapping is a routine minor release, while a new alpha code is reserved for a major version. The practical advice is to avoid looking up a currency by country code if you care about stability, and to use the alpha code, which is the one path the project has promised to keep stable.

The repository shows how the data is managed: there is a data/ directory at the top level and an import-currencies.php script, which is the importer that regenerates the currency set from the standard. A tools/ directory sits alongside it.

PHP 8.2 is the floor, and the older lines exist for end-of-life runtimes

The requirements section is unambiguous: this library requires PHP 8.2 or later. For older runtimes the project publishes separate version lines, and it names them all. PHP 8.1 can use version 0.10, PHP 8.0 can use 0.8, PHP 7.4 can use 0.7, and PHP 7.1, 7.2 and 7.3 can use 0.5.

What makes this worth quoting is the parenthetical that follows. The project states that these PHP versions are end of life and not supported anymore, and that if you are still using one of them you should consider upgrading as soon as possible. The compatibility table is therefore not an invitation to stay on an old runtime. It is a courtesy for the short window during which you can move.

There is one optional performance note as well. Although not required, it is recommended to install the GMP or BCMath extension to speed up calculations. The extension choice matters because brick/math, which this library is based on, performs exact arbitrary precision arithmetic, and the two PHP extensions are the usual ways to get that without falling back to slower paths. The library handles exact calculations on monies of any size, so the size of the numbers is not the limitation; the speed of the arithmetic is what the extension affects.

Installation itself is one Composer command, with the package published on Packagist as brick/money.

bash
composer require brick/money

The composer.json at the top of the repository is where the declared PHP constraint and the dependency on brick/math live, and the README links the brick/math project as the foundation the library is built on.

What this library does not do

The first gap is conversion. There is no exchange rate, no rate provider and no converted() method in the documented API. The CurrencyMismatchException on cross-currency arithmetic is not an oversight to be worked around, it is the design. If your application needs to show a customer a price in their own currency, you need a rate source, a caching policy and a rounding policy for the converted amount, and none of that comes from this library.

The second gap is formatting for display. The library is about values, and the echo examples in the README print a plain representation such as USD 54.99. Locale aware presentation, symbol placement, pluralisation of minor units and internationalisation of the currency name are not part of what is documented, so a team expecting a fully localised money formatter should look elsewhere or add that layer on top.

The third gap is persistence. There is no mention of how a Money should be stored, and the fact that a Money is immutable makes it a candidate for a value object column, but the library does not provide a Doctrine type or a serialisation format. You choose that, and you have to handle the currency identity and the minor unit convention consistently, which is where of() and ofMinor() matter again.

The fourth gap is scope. This is a money and currency library, not a financial one. There is no ledger, no invoice model, no tax handling and no allocation of rounding across line items. A ledger that splits one payment across three line items still has to decide how the remainder cent is distributed, and the library will not make that decision for you.

Editorial conclusion

brick/money fits a PHP application that stores or computes prices in more than one currency and wants the rounding decision to appear at the call site rather than inside a float, since the immutability, the currency mismatch exception and the RoundingNecessaryException are all enforced by the type system. It does not fit a codebase that needs currency conversion from live exchange rates, because the library ships currency data rather than a rate feed, nor one that needs crypto or commodity units. Before adopting it, lock your project to a 0.15.* release cycle as the project status section recommends, and confirm PHP 8.2 or later is available, because the older 0.10, 0.8, 0.7 and 0.5 lines exist for end-of-life PHP versions and are not supported.

Frequently asked questions

How do I install brick/money?

It is installable via Composer with the command composer require brick/money, and the package is published on Packagist as brick/money. The library requires PHP 8.2 or later, and installing the GMP or BCMath extension is recommended to speed up calculations.

What happens if a Money operation needs rounding?

A RoundingNecessaryException is thrown unless you pass a RoundingMode explicitly, for example $money->plus('0.999', RoundingMode::Down). The rounding mode applies only to that one call and is not stored in the Money object, so every later operation that needs rounding must state a mode again.

Can brick/money convert between currencies?

No. Arithmetic between two Money instances of different currencies throws a CurrencyMismatchException, and the library ships currency data rather than exchange rates. Conversion needs a separate rate source and a rounding policy for the converted result.

Which PHP versions does brick/money support?

The current line requires PHP 8.2 or later. For older runtimes the project publishes version 0.10 for PHP 8.1, 0.8 for PHP 8.0, 0.7 for PHP 7.4, and 0.5 for PHP 7.1 to 7.3, and notes that those PHP versions are end of life and no longer supported.

How should I pin a brick/money version?

The project status section says it is safe to lock your project to a given release cycle such as 0.15.*, because a new 0.x cycle is always started when a breaking change is introduced, while non-breaking changes only increment the y part of the version.

Official sources

  1. brick/money on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/brick-money.svg)](https://hysenlabs.com/projects/brick-money)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/brick-money.svg)](https://hysenlabs.com/projects/brick-money)

Was this useful?