When working with money in your Laravel projects, whether it's product prices or invoice total order amounts, we need to be extremely careful not to miscalculate something. Luckily, there are best practices and tools to help us with that, let's explore them in this article.
What we'll cover:
- Typical (and wrong) Money Behavior: Floats
- One Step Better: Integers
- Special PHP Packages For Money
- Laravel Way: Custom Casts
- Currency Conversion
- Specific Laravel Packages/Wrappers
Let's get into it!
Typical (and wrong) Money Behavior: Floats
How do you save data in the database when you need to work with things like product price, or order total?
Since the amount of money is a float - like $3.45, the logical way would be to store it the same way in the database.
$table->decimal('price', 8, 2);// 8 is total digits, 2 is decimal digits
And then, whenever you need to show the price in Blade, you do something like:
Total price: ${{ number_format($product->price, 2) }}
In the code above, we need number_format()
so that 9.1 would be shown as 9.10, with two digits.
In most cases, this approach should work fine, as long as you have one currency in your project and you're not doing a lot of calculations with those fields.
But the way how programming languages and database engines work with calculating floats, you may encounter a rounding problem. Meaning, you may have incorrect calculations by 0.01 if the wrong roundings add up.
Here are a few articles to read more about this problem:
- Floating Point Rounding Errors in MySQL
- Why You Should Never Use Float and Double for Monetary Calculations
- Floating Point Numbers & Currency Rounding Errors
If you are not in the mood to read those articles, and if you want to just trust me, I will simplify it for you:
NEVER STORE MONEY VALUES AS FLOATS IN THE DATABASE.
There, I said it.
Here's another quote for you:
Yes, the possibility of that rounding error happening is very low, but still, it's better to be on the safe side, right? So here are the solutions below.
One Step Better: Integers
Instead of saving money as floats like dollars with cents, you should store them as cents only.
So, instead of having a float/decimal field with a value like 1.23
, you create an integer field with the value 123
.
At first, it may feel weird. In real life no one is calculating money in cents, right? But don't worry, we won't show it on the page as cents. We will transform them while getting from the database and set the cents amount before saving them into the database.
In Laravel, for this, you would typically use Model Attributes: Accessors and Mutators.
class Product extends Model{ protected function price(): Attribute { return Attribute::make( get: fn ($value) => $value / 100, set: fn ($value) => $value * 100, ); }}
Or, in the older syntax (which still works in the latest Laravel version):
class Product extends Model{ protected function getPriceAttribute($value) { return $value / 100; } protected function setPriceAttribute($value) { $this->attributes['price'] = $value * 100; }}
So, when someone fills out the form and enters 123.45
as a value, it is transformed into 12345
in the database. And when the value needs to be shown later in some table, it is transformed back from a database integer value to a human-friendly 123.45
.
Now, there are a few exceptions to notice.
Exception no.1: not all the world currencies have two digits, like cents or pennies. According to this Wikipedia article, there are at least 9 world countries with currencies that have 3-4 decimal digits: Tunisian dinar, Bahraini dinar, and others. If you work with those currencies, then you still need to save data in integer, just divide/multiply by 1000 in case of 3 digits, and by 10,000 in case of 4 digits.
Exception no.2: likewise, some countries have NO decimal digits at all, the same Wikipedia article lists 17 countries like this. For those, no transformations are needed at all, just save money amount as it is.
Exception no.3: you may want to store more decimal numbers if it's needed according to your accounting logic. For example, the price of some very price-sensitive items may be not $0.01
, but $0.0123
, and then rounding the total price at the very last step of the purchase. Then you would store 123
in the database, so you work with the maximum needed numbers of decimal digits, in the lowest denomination.
But even with those exceptions, you get the idea: save the value as an integer, and multiply/divide every time. Works quite well, until it may be not enough.
Special PHP Packages For Money
So far, we've been looking at money as just a number: integer or float. But in real life, money is a more complicated object: what about currency and its rate?
Sure, your project may deal with only one currency, but what about multiple currencies? How to save data then: two fields in the DB? How to perform calculations?
A solution to all those questions lies in a term called value object, or a similar term is data transfer object. Those require a separate tutorial (planned it in the future), but, in short, it means creating an object with properties inside. Money is a very suitable example of a value object.
A quick example from MoneyPHP package:
use Money\Currency;use Money\Money; $fiver = new Money(500, new Currency('USD'));
So, we create a Money
object, which allows us later to transform that object into whatever we want, with many features of the PHP package.
$value1 = Money::EUR(800); // €8.00$value2 = Money::EUR(500); // €5.00$value3 = Money::EUR(600); // €6.00 $result = $value1->add($value2, $value3); // €19.00
There are two popular packages to deal with money in PHP
Here's the example of that second package brick/money
:
use Brick\Money\Money; $money = Money::of(50, 'USD'); echo $money->plus('4.99'); // USD 54.99echo $money->minus(1); // USD 49.00echo $money->multipliedBy('1.999'); // USD 99.95echo $money->dividedBy(4); // USD 12.50
A more interesting example:
$money = Money::of(100, 'USD');[$a, $b, $c] = $money->split(3); // USD 33.34, USD 33.33, USD 33.33
On the surface, both packages perform the same thing: transforming the initial money value into an object, with many transformation features available to use with that object.
There's an interesting short comparison between two packages, in this Github comment. So I will stick to that comment and will show the brick/money
package from here. How would it look in a typical Laravel project?
As you may have understood, storing data in the database doesn't change: you still store it in an integer. And if you work with multiple currencies, you store the currency code, too.
So, in the database migrations we have this:
Schema::create('orders', function (Blueprint $table) { $table->id(); $table->integer('price'); $table->string('currency')->default('USD'); $table->timestamps();});
Then, if we have an order with price 7907
(meaning 79 dollars and 7 cents) and currency USD
in the database, we can have this Controller:
public function show(Order $order){ return view('orders.show', [ 'id' => $order->id, 'price' => Money::ofMinor($order->price, $order->currency)->formatTo('en_US'), ]);}
In the blade, we show the data like this:
Order ID: {{ $id }} ({{ $price }})
Result: "Order ID: 1 ($79.07)"
As you can see, we didn't put the $ sign upfront, we didn't divide by 100, and we didn't format anything manually. The Money package takes care of everything, we just need to call ->formatTo('en_US')
with the locale we want.
Convenient, isn't it?
Laravel Way: Custom Casts
On top of those PHP packages, Laravel gives us even more power: we don't need to create Money::ofMinor()
every time, and we can use Custom Casts, so that $order->price
field would automatically be transformed to a Money object.
We run:
php artisan make:cast Money
It generates the file app/Casts/Money.php which we fill like this:
class Money implements CastsAttributes{ public function get($model, string $key, $value, array $attributes) { return \Brick\Money\Money::ofMinor($attributes['price'], $attributes['currency']); } public function set($model, string $key, $value, array $attributes) { if (! $value instanceof \Brick\Money\Money) { return $value; } return $value->getMinorAmount()->toInt(); }}
And then we assign that class to the Model.
app/Models/Order.php:
use App\Casts\Money; class Order extends Model{ protected $casts = [ 'price' => Money::class ];}
Finally then, in the Controller, we don't need to perform any transformations, we can just pass the $order
object, and perform formatting in the Blade:
class OrderController extends Controller{ public function show(Order $order) { return view('orders.show', compact('order')); }}
Blade file:
Order ID: {{ $order->id }}<br />Price: {{ $order->price->formatTo('en_US') }}
As you can see, $order->price
is already a Money object, and we can use ->formatTo()
directly on that.
Similarly to how Laravel by default has created_at
and updated_at
fields as Carbon objects, so with timestamps in a Blade file, we can do something like {{ $order->created_at->diffForHumans() }}
Notice: in the Cast class, there's a get()
and a set()
method. The first one is clear, but the second one set()
is more tricky, because it depends on what is passed to the price field: in some cases, it may be just an integer (then we just return it as $value
), but maybe you would have it as Money object, then you would need to perform transformation like $value->getMinorAmount()->toInt()
.
Currency Conversion
Now, we're storing the currency, but what's the use of it if we don't convert it to other currencies?
Of course, the topic of conversion is huge in itself and worth a separate long article, but let's see what possibilities we have in the brick/money
package, for example.
The package is shipped with a specific class Brick\Money\CurrencyConverter
, which accepts an exchange rate provider parameter. Several implementations of the rate provider are in the package.
ConfigurableProvider: This provider starts with a blank state, and allows you to add exchange rates manually.
So, in the model, we define this Accessor:
use Brick\Math\RoundingMode;use Brick\Money\CurrencyConverter;use Brick\Money\ExchangeRateProvider\ConfigurableProvider; class Order extends Model{ public function getPriceEurAttribute() { $exchangeRateProvider = new ConfigurableProvider(); $exchangeRateProvider->setExchangeRate('USD', 'EUR', '0.9123'); $converter = new CurrencyConverter($exchangeRateProvider); return $converter->convert( moneyContainer: $this->price, currency: 'EUR', roundingMode: RoundingMode::DOWN ); }}
This Accessor returns $order->price_eur
as a Money object, and then in the Blade, we can do something like this:
Price: {{ $order->price->formatTo('en_US') }}({{ $order->price_eur->formatTo('en_US') }})
Result: "Price: $57.15 (€52.13)"
Also, there are other Currency Providers:
- PDOProvider: reads exchange rates from a database table
- BaseCurrencyProvider: for the quite common case where all your available exchange rates are relative to a single currency
Also, you can write your own provider, implementing the ExchangeRateProvider
interface and the method getExchangeRate()
.
With all those providers, you can also implement to get the exchange rates from external APIs like this one, or datasets like this XML provided by the European Central Bank, and crawl the data regularly from there into your database.
Specific Laravel Packages/Wrappers
Finally, we get down specifically to Laravel tools around money. So far, we've been discussing brick/money
and moneyphp/money
which are PHP packages, is there anything for Laravel?
Glad you asked.
akaunting/laravel-money - This package intends to provide tools for formatting and conversion of monetary values in an easy, yet powerful way for Laravel projects.
It's a standalone package, meaning it's not a wrapper over any PHP package from above. On top of creating the Money object, it also provides Helpers, Blade directives, and Components:
Money::USD(500);money(500, 'USD')@money(500, 'USD')<x-money amount="500" currency="USD" /><x-currency currency="USD" />
Also, there are a few wrapper packages like cknow/laravel-money which abstracts MoneyPHP mentioned above, also adding Laravel features like custom casts, helpers, and Blade directives.
Conclusion: Money is Hard
These are just a few possible solutions and tools how to store and format money value in your Laravel projects.
Real-life scenarios are even more difficult: we haven't covered examples like calculating the invoice values with taxes and fraction values of the items, rounding in specific conditions, and many more parameters provided by the packages above.
But I hope this article will give you enough overview to understand the ecosystem and the context, and then you would dive deeper based on your specific project needs.
great article, I've being sting in the past storing money as a string or floats then wondering why calulations are out with rounding.
Thank Mr. Povilas. Great article.
I did the casting, the display is working fine, I used this:
So I'm getting the price in my blade directly.
But for saving the price, I didn't figure out how to do it, I did the casting and my table price column is integer. For example when I save 27.8, it is saved in the database as 28.
Got it:
sorry for keeping posting. I'm using Nova, I'm trying to make this work with my frontend and my backend which is Nova admin panel.
Now I'm using: in the Order modle:
in the observer:
Sorry I'm not a Nova user so I'm not sure what that tool does with the fields automatically, that may affect the code and may be different from my example.
I think you should look at Nova docs and how it deals with Money, and follow my article only for theoretical knowledge how it works under the hood.
Thanks! For any one looking to display the curreny in the right format, the currency field withen Nova have functions to do that:
you can do somthing like this also:
I'd be more relaxed with saving floats in the database. It depends. For example, you don't show arbitrary precision amounts in the accounting industry but always round them. Of course, you don't do rounding on the database side but in PHP.
And in the case of Akaunting, because users can change the precision from UI whenever they want, you can't save it as an integer. That's why we made the
akaunting/laravel-money
package, which works like a charm for 200K+ Akaunting users ;)Best regards, Denis Duliçi
Great,
what about DB migration , what is the column type of price at database ?
Integer.
Very cool this article. How do you suggest using integrated with livewire? For example, how would the livewire property look to show the formatted value in the input and then change it to an integer to persist in the base?
Interesting question. I would probably do the conversion on Eloquent level, with Casts and accessors/mutators if needed, Livewire would be the layer just for presenting the data.
Great article! Looking forward to more articles that dives deep more about how to use these packages for complex calculations, currency conversion based on various business requirement scenarios
Great article! But, while I agree that the FLOAT type should not be used for storing money, the DECIMAL type is perfectly suitable in my opinion, as these types are not the same. In MySQL, there are actually two groups of types to consider:
Fixed-Point Types (Exact Value) - DECIMAL, NUMERIC
Floating-Point Types (Approximate Value) - FLOAT, DOUBLE
So, DECIMAL and NUMERIC types are advised to be used with monetary data by the documentation as they use Precision Math for calculations.
I don't think there is any difference between using INTEGER and DECIMAL, except that you need to manually transform values before displaying and storing them in the database in the case of an INTEGER type, but I'm curious if anyone has an example where DECIMAL type will give incorrect results compared to INTEGER.
I'm using brick/money package for life insurance comparator app (to find best life insurance to customer). DB tables - decimal columns, no currency conversions. No problems with casting, storing, computing, ... and it's used with spatie/laravel-data and Laraval Nova packages. To deal with money try brick/money package at first :)
In my case, I read prices from a 3rd party api as a string of the value and a currency code. To store it as an integer, I need to multiply it by 100 but if I'm not wrong, even this causes rounding errors. For example:
So I tried to parse the string manually, but for many currencies with different locales, this may be a headache. Am I wrong with the above statement? If not, how can I supply integer values to those packages that use integers with their constructors?
Try this, some explanation here
(int) round((float) "0.29" * 100, 0);
Or you can use brick/money package:
Money::of("0.29", "EUR")->getMinorAmount()->toInt();
Thanks, I knew there shouldn't be such big rounding errors =)
So I can assume that rounding errors won't be high enough to change a digit when multiplying with 100. In my case, the problem was direct casting to
int
which only takes the whole part into consideration.I liked brick/money as well, which accepts non-integer arguments to factory method. Probably going to use it to handle multi-currencies. It also has many strong methods.
Just wanted to let you know that they stole this post. https://medium.com/@laravelprotips/handling-money-in-laravel-php-essential-tips-014b5ee83336#:~:text=Integers%3A%20A%20Smarter%20Approach%20for%20Money%20in%20Laravel&text=Rather%20than%20keeping%20a%20float,this%20method%20offers%20better%20precision.
This is sad, but not a lot we can do... Especially on medium as they don't really care what posts are in there.