Dealing with Money in Laravel

September 30th, 2024 • 6 minutes read time

Dealing with monetary values in any application is something you have to get right the first time. Without the correct approach, you risk displaying prices incorrectly or worse, making miscalculations.

Here's how to work with money properly in Laravel.

The golden rule when storing prices is never use a float or a double. This might seem strange at first because we typically think of money represented as a float value (like 5.67 for $5.67).

When dealing with money, we need precision. Floating point numbers have rounding errors. For basic outputting of prices, you could use a float, but any kind of comparison or calculation with these values could result in weird behaviour.

Aside from this, it's just plain awkward to calculate with floats. Let's see an example.

Here's what defining a column on a Product model would look like with the wrong approach:

Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->float('price');
    $table->timestamps();
});

Now imagine we have a 25% sale and want to calculate the discount a customer would receive on an item priced $8.99. Here's a rough example of calculating this:

$product = Product::find(1);

$couponPercentage = 25;

$discount = ($product->price * $couponPercentage) / 100; // 2.2475

So the end result is 2.2475. The actual amount we'd want to take off is 2.25. And now we're left figuring out how to properly round this number with 4 decimal places. Imagine if things got more complex than this? (let's not).

The solution to all this is to use an integer to store money in cents (or whatever monetary unit you use).

Let's change up our migration:

Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->unsignedInteger('price');
    $table->timestamps();
});

We use an unsignedInteger here, which can hold a larger positive value and no negative value, since we don't need negative prices.

To populate prices, you'll now need to multiply the price by 100. Therefore, 8.99 is stored as 899.

Let's look at the same example from earlier, where we want to calculate a 25% discount on this, and see how much easier it is.

$product = Product::find(1);

$couponPercentage = 25;

$discount = ($product->price * $couponPercentage) / 100; // 224.75

The discount is now 224.75. Much easier to round up or down and get a cent value back. Let's try it:

dd(round($discount)); // 225.0

The result is now 225, which, if we divide by 100, gives us a clean discount value:

dd(round($discount) / 100); // 2.25

If this still seems tricky, you're right. Dealing with numbers can get messy, and we've not even covered formatting this discount to a currency value yet, let alone subtracting it from the total price to give the final amount to pay.

That's where the laravel-money package comes in.

The laravel-money package is a nice Laravel wrapper for the moneyphp/money package. It makes working with money in Laravel a breeze.

Let's install it and look at the previous example to see how easily we'll be able to calculate discounts.

composer require cknow/laravel-money

This package gives us a few ways of interfacing with it. Let's start with the Facade.

Here's the full example of working out a 25% discount from earlier:

$product = Product::find(1);

$couponPercentage = 25;

$discount = Money::USD($product->price)
    ->multiply($couponPercentage)
    ->divide(100)
    ->format();

dd($discount); // "$2.25"

So, as well as doing all the hard work behind the scenes for us, the format method has also formatted this into a price for us using the dollar currency.

Let's calculate the discount and the final price.

$discount = Money::USD($product->price)
    ->multiply($couponPercentage)
    ->divide(100);

$finalPrice = Money::USD($product->price)->subtract($discount);

dd($discount->format()); // "$2.25"
dd($finalPrice->format()); // "$6.74"

Notice how we've passed the $discount object into the subtract method to subtract the calculated discount from the pre-discounted price. Finally, we finish by using format on both values to get the dollar amount.

It's just as easy to use other currencies with this package. In fact, there are a few ways.

You can publish the configuration for this package and adjust the currency in the published config/money.php file.

php artisan vendor:publish --provider="Cknow\Money\MoneyServiceProvider"

Here's the config file, where you can either override the default value, or add a currency key to your config/app.php file.

return [
    'locale' => config('app.locale', 'en_US'),
    'defaultCurrency' => config('app.currency', 'USD'),
    'defaultFormatter' => null,
    'currencies' => [
        'iso' => ['RUB', 'USD', 'EUR'],
        'bitcoin' => ['XBT'],
        'custom' => [
            'MY1' => 2,
            'MY2' => 3
        ]
    ]
];

You can use the money helper anywhere, allowing a currency to be passed in. This works nicely for multi-currency apps where the currency is dynamic:

money($product->price, 'GBP')->format(); // "£8.99"

All the same methods work on the money helper since it returns the same Money object we've already used.

My least favourite option is to continue using the Money class directly, but you're able to choose any currency with this:

Money::GBP($product->price)->format(); // "£8.99"

So far, we've been manually pulling the $product->price and passing it into either the Money object or using the money helper. A much more convenient method going forward is to automatically cast the price column in your model.

Here's what it looks like under our Product model:

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Cknow\Money\Casts\MoneyIntegerCast;

class Product extends Model
{
    use HasFactory;

    protected $casts = [
        'price' => MoneyIntegerCast::class . ':USD',
    ];
}

We'll cover how to dynamically adjust this currency on a per-model basis shortly, but first, let's see the difference this makes now.

Returning to the example of calculating a discount, here's how it would look:

$discount = $product->price
    ->multiply($couponPercentage)
    ->divide(100);

$finalPrice = $product->price->subtract($discount);

The price attribute of the Product model is now cast to a Money object, so we no longer have to manually add that value. This is also helpful when we get to outputting formatted values in Blade (or Livewire), which we'll look at later.

What happens if you have a bunch of products, plans (or whatever you're selling) with different currencies?

In this case, you can add a currency column to your database table:

Schema::table('products', function (Blueprint $table) {
    $table->string('currency', 3)->default('USD');
});

And then, in your model, define the cast like this:

class Product extends Model
{
    use HasFactory;

    protected $casts = [
        'price' => MoneyIntegerCast::class . ':currency',
    ];
}

This will now use the currency column to determine which currency to use for the particular model you're working with.

There are a bunch of Blade directives registered when we installed this package, which you can find in the documentation. However, where this really shines is the automatic casting to display prices.

Let's imagine a controller we're using to display the product:

class ProductShowController extends Controller
{
    public function __invoke(Product $product)
    {
        return view('products.show', [
            'product' => $product
        ]);
    }
}

Inside the products.show template, we can output the price attribute of the product, and it'll be automatically formatted for us due to the cast we created earlier.

<div>
    Price: {{ $product->price }} <!-- $8.99 -->
</div>

To be fair, this is true for Money objects in general. So, we could also manually pass down the discount to display on this page too:

public function __invoke(Product $product)
{
    $couponPercentage = 25;

    $discount = $product->price
        ->multiply($couponPercentage)
        ->divide(100);

    $finalPrice = $product->price->subtract($discount);

    return view('products.show', [
        'product' => $product,
        'discount' => $discount,
        'finalPrice' => $finalPrice,
    ]);
}

And here's the template:

<div>
    <div>Price: {{ $product->price }}</div>
    <div>Discount: {{ $discount }}</div>

    <div>You pay: {{ $finalPrice }}</div>
</div>

We've covered the majority of how you'll work with money in Laravel here, but there's a load more the laravel-money package can do.

Now you've grasped the basics, refer to the documentation on everything else you can do.

Most importantly, you'll sleep better at night knowing you're dealing with money in Laravel the right way.

If you found this article helpful, you'll love our practical screencasts.
Author
Alex Garrett-Smith

Comments

No comments, yet. Be the first!