Storing Multi Currency Prices In Laravel
Recently I was involved with building a new module for our internal Intranet. It is responsible for storing all the details of our subscriptions; name, start/end dates, price, notes... All simple enough except when it came to storing the price. We have subscriptions which are paid in all different currencies. The first idea was a two column approach, price
and currency_id
with a foreign key to a table of currencies. This worked but it had a bad smell. It was a 1:1 relationship with a table which contained only a single value: the currencys three letter code.
The next idea was to store the currency directly alongside the price: "USD100" or "GBP50". Note that this is $1.00 and £0.50, not $100 or £50. Money should always be stored as an integer of the smallest denomination. This had the nice side affect that you could look at the database and the currency was obvious.
Now all that was needed was to sprinkle some Laravel magic in. Developers shouldn't need to worry about serialising data, we needed an Attribute cast:
<?php declare(strict_types=1);
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Money\Currencies\ISOCurrencies;
use Money\Currency;
use Money\Money;
use Money\Parser\DecimalMoneyParser;
class MoneyCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes): ?Money
{
if ($value === null) {
return null;
}
$currencyCode = substr($value, 0, 3);
$amount = substr($value, 3);
return new Money($amount, new Currency($currencyCode));
}
public function set($model, string $key, $value, array $attributes): ?string
{
if ($value === null) {
return null;
}
if ($value instanceof Money) {
return $value->getCurrency()->getCode() . $value->getAmount();
}
$currencyCode = substr($value, 0, 3);
$amount = substr($value, 3);
$money = (new DecimalMoneyParser(new ISOCurrencies()))
->parse($amount, new Currency($currencyCode));
return $money->getCurrency()->getCode() . $money->getAmount();
}
}
When the price column is requested it is first mutated into an instance of Money
. The inverse happens when the value is saved. The Money
object is supplied by the excellent moneyphp/money package.
Note in this case that the cast also supports nullable fields but that may not apply.
Ship It!
While there are existing packages [1, 2] they didn't really meet our requirments. There are also more fully featured packages like lukeraymonddowning/mula which includes a cast very similar to the one above. It also brings the kitchen sink with it. Sometimes you need something which does one thing and does it well:
composer require freshleafmedia/laravel-money-cast
For more examples and usage see GitHub