Handling Money In Laravel
Money is important. And it's serious. It needs to be done right.
TLDR; Don't use floats for money, use integers. Use currency. Use a specific money type. Use a caster to persist/retrieve in the database
I recently did a code test for a FinTech who were absolutely nuts about 'clean code'. Really picky about structure, to the extent that I felt their obsession with architecture was quasi-religious. Don't get me wrong, I have 'opinions' on code structure, but being pragmatic and getting the job done are also important. I always see problems when teams treat rules as laws - never to be bent or broken whatever the cost.
But the thing that struck me the most was their treatment of money values - they were using floats.
What's the issue? Well, floating point arithmetic has well known issues that make it, um, difficult, for handling money.
Have a look at the following code.
$a = 35.00;
$b = 34.99;
echo $a - $b;
> 0.009999999999998
Not ideal. There are obviously ways around this (like using bcmath for example, or rounding the answer) but it's probably only a matter of time before someone messes this up (by adding up items in a loop and compounding the rounding errors) and you're left tracking down an nasty bug that only affects certain cases, and is very difficult to explain to the Accounts department (it works in Excel!).
The answer to this is to deal with money as an integer - the value of the smallest currency unit, for example cents, pence, sen, escudos etc. We should handle money values as integers internally, and then format them correctly for display.
$a=3500;
$b=3499;
$c = $a - $b;
$formatter = new NumberFormatter('en_GB', NumberFormatter::CURRENCY);
echo $formatter->formatCurrency($c / 100, 'GBP');
> £0.0
The second problem is that money is not just a numeric value. In reality, money is a number and a currency. I would argue that even if you are only dealing with money in one country, it's easier to do this right at the start than possibly try and retrofit currency into your app at a later date.
Fortunately, PHP has several excellent libraries that can deal with this for you. You can use moneyphp/money (which implements Fowler's Money pattern) or my preference, brick/money. Laravel already uses brick/money in Cashier and Spark, so it makes sense to stick with it.
I'm not going to go into massive detail about how to use brick/money as it already has great documentation, but the missing part of the equation for Laravel programmers might be how to deal with persisting money values to the database.
I like to use a combination of a JSON database field and a custom caster to handle this. Using a json field means we can store the currency and value in a single field (avoiding hacks like having two fields for amount and currency and hard-coding something into the caster).
A sample migration
Schema::create('items', function (Blueprint $table) {
$table->id();
$table->json('price');
$table->timestamps();
});
And a caster to handle mutating and accessing
<?php
namespace App\Casts;
use Brick\Money\Money as BrickMoney;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
class Money implements CastsAttributes
{
/**
* Cast the given value.
*
* @param array<string, mixed> $attributes
*/
public function get(Model $model, string $key, mixed $value, array $attributes): BrickMoney
{
$fields = json_decode($value);
return BrickMoney::ofMinor($fields->amount, $fields->currency);
}
/**
* Prepare the given value for storage.
*
* @param array<string, mixed> $attributes
*/
public function set(Model $model, string $key, mixed $value, array $attributes): mixed
{
/** @var BrickMoney $value */
return json_encode([
'amount' => $value->getMinorAmount()->toInt(),
'currency' => $value->getCurrency()->getCurrencyCode()
]);
}
}
Then all we need to do is attach this caster in the model
<?php
namespace App\Models;
use App\Casts\Money;
use Illuminate\Database\Eloquent\Model;
class Item extends Model
{
protected $casts = [
'price' => Money::class
];
}
Now we are able to create money types and store them safely in the database.
$price = \Brick\Money\Money::ofMinor(1001, 'USD');
$item = new Item([
'price' => $price
]);
$item->save();
And as we've stored the money value as a json field, we can access the amount and currency in queries
$item = Item::where('price->amount', '>', '1000');
So, to conclude. Don't use floats for money. Use a dedicated money class. Use a custom caster to save money to the database.