brick/math: exact arithmetic for PHP that refuses to guess, and a parser for the cases you do not trust
Arbitrary-precision arithmetic library for PHP
At a glance
- What is it?
- brick/math gives PHP three immutable arbitrary-precision number types backed by GMP, BCMath or a pure PHP fallback, and its most consequential decision is a negative one: a conversion that would lose precision raises an exception instead of rounding quietly. The second is a parser that exists because the ordinary factory will happily turn a nine character string into a number with a billion digits.
- Who is it for?
- brick/math is the right choice for money, for identifiers larger than a machine integer, and for any calculation where a wrong answer is worse than a raised exception, because the conversion policy is the feature and it holds across every method rather than only at construction. Do not reach for it out of habit for arithmetic that fits in a double, where the object allocation and the string conversions cost more than the wrongness would.
- 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 5 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 September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Three types under one abstract base, with constructors you cannot call
The shape of the library is three classes under a common abstract parent. An integer type, a decimal type and a rational type, where the rational is always reduced to lowest terms, so a fraction written as two sixths becomes one third before you ever see it. The abstract base defines the behaviour they share, and the readme lists it: a factory method to obtain an instance, sign queries, comparison queries, and a handful of operations including the minimum, the maximum, a sum and a string conversion. Below that sit the three concrete classes. Two design decisions here are worth more than the class list itself. The first is that the constructors are not public, so an instance can only be obtained through the factory. That is a small constraint with a large payoff: if the only way to build a value is a method that centralises the parsing and the conversion rules, then every value in your program has been through the same validation, and there is no way to bypass it by reaching for a constructor. The second is immutability, and the readme explains why in one sentence: a value never changes, so it can be safely passed around. Every method that returns a number returns a new object and leaves the original alone, which is what makes the chaining style at the end of the example work, and what makes it safe to hold a constant in a property and use it in a dozen places without any risk of one of them altering it. Combined with the inheritance hierarchy, the result is a small object model rather than a set of functions, which is the main thing this library offers over the alternatives at the end of this article.
GMP, BCMath or pure PHP, selected at runtime
The implementation detail that shapes everything else is that this is not one calculator but three behind one interface. The library uses the GMP extension when it is available and the BCMath extension when it is available, and falls back to an implementation written in PHP when neither is. The selection happens at runtime and the readme says the fastest available implementation is chosen automatically, with a recommendation to install one of the two extensions for speed while noting the library works without either. That architecture has a hard requirement that a single implementation would not: the three backends have to agree. A number parsed on a machine with GMP and the same number parsed on a CI runner without it must be identical, in value, in scale and in how they round, or every test that passes locally fails in production and vice versa. The readme states that the number of digits is limited only by available memory and processor time, and the top-level file list gives a hint about how that consistency is checked, since there is a script named for random tests sitting outside the test directory alongside the unit tests, which for a library with interchangeable backends is where property-based cross-checking would live. The readme does not describe it, so that is inference rather than documentation, and the tool is worth opening if you are extending the library. The cost of the design is the extension requirement. The library needs PHP 8.2 or later, which is a real floor for an application stuck on an older runtime, and it needs you to think about which extension your deployment image has installed, because a production box without GMP gets the pure PHP path and a different performance profile from your laptop.
The conversion matrix, and the fraction that throws
This is the part of the readme worth reading twice, because it is the library's moral centre and it is expressed entirely through examples. The factory accepts several kinds of input: another number object, a native integer, or a string holding an integer, a decimal or a fraction. What it will not do is accept a representation it cannot represent exactly in the class you asked for. So an integer can be built from an exponent form, because a thousand is a thousand, and from a decimal that happens to be whole, because one point zero zero is one. But a decimal that is not whole is refused, and the readme names the exception. A decimal can be built from a fraction that terminates, so an eighth becomes exactly zero point one two five, and a fraction that does not terminate is refused, and one third is the canonical example. A rational can be built from a terminating decimal, and the readme shows both a one point one becoming eleven tenths and a one point one five becoming twenty three twentieths, both exact. Now count what that design buys. Every one of those refusals is a place where the naive alternative rounds silently and hands you a number that is wrong in a way you will not notice until an invoice does not reconcile. The library chooses an exception over a plausible answer, and it does so consistently, which is the property that makes it usable in code that has to be correct rather than code that has to run. It also means the exception is a normal control-flow event in well written application code, because converting a user-supplied quantity into a decimal is a step that can legitimately fail, and a library that makes that failure visible is doing you a favour.
Floats are refused outright, and there are two defensible ways in
The factory does not accept native floating point values at all, and the readme gives the reason in a sentence: casting a float to a string can be lossy. That is the correct reason and it is worth dwelling on, because a float is the only numeric type in most languages that does not know what it holds. The binary representation of one tenth is a repeating fraction, so the double nearest to it is not one tenth, it is a number slightly larger, and any conversion to decimal has to choose. The library's answer is to make you choose explicitly, and it offers exactly two choices. The first produces the exact value the float holds, which for one tenth is a fifty digit decimal beginning with a long run of zeros and nines and ending in the exact binary fraction. The second produces the shortest decimal that converts back to the same float, which for the same input is one tenth, the number you meant.
BigDecimal::fromFloatExact(0.1); // 0.1000000000000000055511151231257827021181583404541015625
BigDecimal::fromFloatShortest(0.1); // 0.1Both are correct and they answer different questions. The exact form is what you want when the float arrived from somewhere that already computed with floats and you need to preserve exactly what was computed, so that your arithmetic agrees with theirs digit for digit. The shortest form is what you want when a human or a JSON payload wrote the value and you want the number they intended. Neither is a default, and the absence of a default is the point. A library that silently picked one would make your result depend on a choice you did not know was being made, and the two differ in the fourth decimal place onward for values like one tenth, which is exactly the range where currency arithmetic goes wrong. The same reasoning applies to every other float you have ever accepted from a web request: this library makes you say which one you meant, and that conversation is worth having once.
A nine character input can allocate a gigabyte, which is why the parser exists
This is the most practically important section in the readme and it is about denial of service rather than about arithmetic. The factory places no hard limits on its input. A string with millions of digits is accepted as it stands, and a number written in exponential notation is expanded to its full length, so the string one followed by e and a billion is accepted and yields a number with a billion digits. That is a few bytes of input producing a gigabyte of memory and an unbounded amount of processor time, and it is the sort of thing that turns a form field into an outage. The readme names the case exactly: input from an untrusted source such as an HTTP request. The fix is a second entry point that requires two things from you rather than inferring them. The first is the permitted syntax, given as a case from an enumeration. Plain integers are always accepted and each case adds one more feature on top, so integers only, integers and decimals which the readme calls typical for monetary input, integers and decimals and exponents which it notes accepts every JSON number, integers and fractions, or the full syntax the factory accepts. The second is a maximum digit count, and the detail that makes it work is that the count is applied both to the digits as written and to the digits of the resulting number.
BigDecimal::parse('123.45', allowedSyntax: NumberSyntax::DECIMAL, maxDigits: 20); // 123.45
BigDecimal::parse('1.2e3', allowedSyntax: NumberSyntax::DECIMAL, maxDigits: 20); // NumberFormatException (exponent not allowed)
BigDecimal::parse('1e1000000000', allowedSyntax: NumberSyntax::SCIENTIFIC, maxDigits: 20); // NumberFormatException (too many digits)Counting the expanded value is what closes the hole, because it means the rejection happens before the number is ever built rather than after a gigabyte has been allocated, and the readme says so in those terms. So the two failure modes are separate and both are refused: a syntax you did not permit, and a magnitude you did not permit. For any application taking numbers from a request, the parser is not an optional hardening step. It is the only safe way in, and the fact that the library separates it from the factory rather than making the safe path the default is defensible precisely because the factory is for values you already trust, such as literals in your own code and configuration.
The unrestricted path is in every method parameter, not just the constructor
Here is the part that turns the previous section from a warning into a rule. Every method that accepts a number, the arithmetic operations named in the readme and others, accepts the same set of types the factory accepts. So a method that multiplies will accept a native integer, a string, or another number object, and the string is converted using the factory's rules, which are the unrestricted ones. The readme states this explicitly and then gives the instruction: for untrusted strings, parse them first and pass the resulting number to the method. The examples show the same conversion policy applying in an argument position, with a whole number from a decimal string accepted, a decimal string that is not whole refused, and a decimal multiplied by a native integer producing a decimal result. Read the shape of that last example carefully, because it is a small type surprise: multiplying a decimal by a native integer returns a decimal, not an integer, so the result type follows the left operand rather than the operation. That is normal for this kind of library and it is the kind of thing worth knowing before you assign the result to something typed more narrowly. The security consequence is the point of this section. The unrestricted conversion is not confined to construction, so validating at the boundary is not sufficient if your validated value is later turned back into a string and handed to an arithmetic method. The discipline has to be that untrusted input becomes a number object once, at the edge, and that object is what travels through your code. Any string that reaches an arithmetic call has to have come from a literal, from configuration you control, or from the parser.
Version 1.0 in September 2026, on a PHP 8.2 floor
The release record is short and it is the newest thing about the project. There is a 1.0.0 published on 2026-09-12, a 0.20.0 on 2026-08-28 and a 0.19.1 on 2026-08-08, and the last push to the repository was on 2026-09-25. So this is a library that spent a long time in pre-release, moved through several versions in the closing weeks, and has now cut a first stable version while still committing. That is the best possible signal for an adopter, because a first stable version is a promise about the public surface rather than about the implementation, and this library's public surface is small enough that the promise is keepable. It also means the pre-release line is a reasonable thing to be already running, and the upgrade to 1.0 is worth reading the changelog for rather than assuming is a version bump. The repository is a conventional PHP package: a changelog, a licence, a composer manifest, a static analysis configuration, a test configuration with a separate bootstrap file, a coverage configuration, and separate directories for the source, the tests and tooling. There is no separate baseline file for the static analysis, which for a library of this age either means the analysis is clean or that the accepted exceptions live inside the configuration, and in both cases it is a better position than a recorded backlog. The licence is MIT, so there is nothing to reason about before using it in a commercial application, and the runtime floor of PHP 8.2 is the only hard requirement beyond the optional extensions.
The alternatives: the built-in extensions, and the type error they all share
It is worth naming what you would otherwise use, because the comparison is closer than it first appears. PHP ships arbitrary-precision arithmetic itself, in two extensions. One provides integer and rational operations at C speed. The other provides decimal arithmetic through a procedural set of functions that take and return strings, and it is the traditional choice for money in PHP for that reason. What neither gives you is an object model. You get functions, so there is no base class, no immutability, no factory that centralises parsing, and no way to make a method accept any of several representations and validate them consistently. That is the gap this library fills, and it is a real one in a codebase where a number can arrive as a string from a form, an integer from a database column and a rational from a calculation. The second alternative is the native float, which is what most PHP code uses and which is wrong for money and for anything exceeding about fifteen significant digits, for reasons this library's float handling exists to make explicit. The difference in approach between this library and the built-in extensions is therefore not speed. It is policy. The extensions will happily round a conversion and hand you a plausible number. This library throws, offers you two explicit choices when a float genuinely has to be converted, and gives you a bounded parser for input you do not control. If your arithmetic is simple enough that rounding never bites you, the built-in extension is less machinery. If it ever does bite, this library converts a silent wrong answer into a visible exception, and that is the trade the whole design is built around.
Editorial conclusion
brick/math is the right choice for money, for identifiers larger than a machine integer, and for any calculation where a wrong answer is worse than a raised exception, because the conversion policy is the feature and it holds across every method rather than only at construction. Do not reach for it out of habit for arithmetic that fits in a double, where the object allocation and the string conversions cost more than the wrongness would. Two things to check before you adopt it. The runtime floor, because the library requires PHP 8.2 or later and that is the first thing that will rule it out on an older application. And your own input paths, because the factory method places no limits on what it accepts, and every arithmetic method that takes a number will convert a string through that same unrestricted path, so any value arriving from a request has to go through the parser with an explicit syntax and a digit limit first. The rest is routine: one Composer package, three classes under one abstract base, an MIT licence, and a version 1.0 cut in September 2026 after a long pre-release line.
Frequently asked questions
How is brick/math installed, and what does it need?
It is a Composer package installed with one require command, and it requires PHP 8.2 or later. It works without any extension by falling back to a pure PHP implementation, and the readme recommends installing either the GMP or the BCMath extension for speed, with the fastest available implementation selected at runtime.
Why does BigDecimal::of('1/3') throw an exception?
Because one third has no terminating decimal representation, and the library refuses to round silently. A fraction that terminates, such as an eighth, converts exactly to a decimal. The readme's stated policy is that a value is accepted as long as it can be safely converted to the requested class, and anything else raises a rounding exception.
Why can I not pass a float to the factory method?
Because casting a float to a string can be lossy, so the factory refuses them. There are two dedicated methods instead: one producing the exact value the float holds, and one producing the shortest decimal that converts back to the same float. For one tenth those are a fifty digit decimal and one tenth, and you choose which one you meant.
What is parse() for, and when do I need it?
For input you do not control. The factory has no limits, so a short string in exponential notation expands to its full length and can allocate a huge amount of memory. The parser requires you to state the permitted syntax and a maximum digit count, and that count applies to the expanded value as well as the input, so an oversized value is rejected before it is built.
Do I have to parse() every value that comes from a request?
Yes, once, at the boundary. Every arithmetic method accepts strings and converts them through the same unrestricted rules as the factory, so the safe pattern is to turn untrusted input into a number object with the parser at the edge and pass that object everywhere afterwards. The readme says the same rules apply to method parameters as to construction.
Official sources
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.
[](https://hysenlabs.com/projects/brick-math)