Working with monetary values¶
"If I had a dime for every time I've seen someone use FLOAT to store currency, I'd have $999.997634" -- Bill Karwin
Floating-point calculations, often used in financial transactions, are tricky and error-prone because of how computers process them. Small mistakes can accumulate and cause severe damage to a business. For example:
var_dump(0.0.1 + 0.05 === 0.06)
// bool(false)
Money can be represented using simple integers ($1.23 stored as 123), but that causes more trouble than it solves:
* Integers have limited range (they can overflow silently)
* Subunits can change over history
* It's tricky to handle rounding, e.g. taxes: €123.45 * 1.21 equals €149.3745 but that's not a valid amount. You have to
round it to €149.37.
* Converting floats to int is tricky. Consider: (int) (4.10 * 100)
which results in 409, not 410. The (int)
cast
does not handle rounding. It strips the fractional part!
Using a dedicated Money library¶
Money is a perfect candidate for an immutable value object. Numbers are meaningless when not combined with a currency. In Patterns of Enterprise Application Architecture, Martin Fowler describes the Money Pattern. There are endless reasons why not to represent money as a simple value (e.g. floating point calculations and rounding errors), so the Money Pattern describes a class encapsulating the amount and currency.
“A large proportion of the computers in this world manipulate money, so it’s always puzzled me that money isn’t a first-class data type in any mainstream program- ming language.” – Martin Fowler
It also defines all the mathematical operations on the value with respect to the currency. It stores the amount as integer in cents, the lowest possible factor of the currency. We can not divide it more.
In this module we use the moneyphp/money library that implements this pattern. Some advantages are:
- Money objects are immutable
- Easy to use as Doctrine embeddable:
@ORM\Embedded(class="\Money\Money")
- Easy money formatting to different locales (with
IntlMoneyFormatter
) - Easy conversion between currencies using converts (e.g. using Swap)
- Easy to sum up money, find a minimum/maximum/average, do allocations, ...
- Implements
JsonSerializable
to convert money to JSON to exchange monetary data with other systems.
As a consequence, monetary values in the MySQL database are stored in cents instead of a decimal number. A database
column is added for the price_amount
and price_currency_code
. This is similar to how other libraries work,
e.g. Stripe also expects monetary values in cents and describes it
an amount as "A positive integer representing how much to charge in the smallest currency unit (e.g., 100 cents to charge $1".
Integrating moneyPHP in Symfony, Doctrine and Twig can be done using TheBigBrainsCompany/TbbcMoneyBundle.
Examples¶
$fiveEuro = Money::EUR(500); // €5
$tenEuro = $fiveEuro->add($fiveEuro); // €10
$net = new Money(123, new Currency('USD')); // $1.23USD
$gross = $net->multiply('1.10', Money::ROUND_UP); // Add 10% to 1.23 = 1.353, and round it up
In a Doctrine entity:
// Product.php
// This will create a MySQL column `price_amount` and `price_currency_code`
/**
* @ORM\Embedded(class="\Money\Money")
*/
private Money $price;
Or using our Twig helper function to render values:
{{ cart.subTotal|format_money_decimal() }} // 5.00
{{ cart.subTotal|format_money() }} // €5.00
Warning
Currently the Commerce module only supports a single currency: EURO