Storing money as floats in MySQL or PHP is a silent production bug waiting to explode. You think 19.99 * 100 converts $19.99 to 1999 cents, but PHP returns 1998.9999999999998. Cast that float to an integer, and you just charged your customer $19.98. You've lost a cent, your accounting ledger is off by $0.01 on hundreds of orders, and auditing tax records becomes a nightmare.
MySQL DECIMAL(10,2) fixes storage inside the database, but the moment Eloquent hydrates that string into PHP, developers end up calling (float) $model->price to perform basic arithmetic. Arithmetic on floating-point numbers violates IEEE 754 precision guarantees. The fix isn't scattering round() across thirty controllers. The fix is storing money strictly as integer minor units—cents, pence, yen—and wrapping that behavior in Laravel custom casts and value objects.
Database Schema and the Minor Unit Principle
Always store monetary values as integers representing the smallest currency unit. In migration files, use unsignedBigInteger or bigInteger instead of decimal or float.
Why bigInteger instead of standard integer? A signed 32-bit integer caps out at 2,147,483,647 minor units. In US Dollars, that's roughly $21.4 million. That sounds like a lot until you start aggregating annual account totals or building multi-tenant SaaS dashboards. A 64-bit bigInteger handles up to 9 quintillion minor units, giving you plenty of headroom.
Multi-currency databases require two columns per price field: the integer minor unit amount and the ISO 4217 three-letter currency code.
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->bigInteger('amount');
$table->string('currency', 3)->default('USD');
$table->timestamps();
});Minor units differ across currencies. USD, EUR, and GBP use 2 decimal places (1 dollar = 100 cents). JPY, KRW, and UGX use 0 decimal places (1 yen = 1 yen). KWD, BHD, and OMR use 3 decimal places (1 Kuwaiti Dinar = 1000 fils). Hardcoding a division factor of 100 across your entire codebase breaks the instant you launch in Japan or Kuwait.
Building an Immutable Money Value Object and Custom Cast
Instead of juggling integers and strings separately, create a dedicated Money value object in PHP 8.3. This keeps currency rules co-located with calculation logic.
namespace App\ValueObjects;
use InvalidArgumentException;
use Illuminate\Support\Number;
readonly class Money
{
public function __construct(
public int $amount,
public string $currency = 'USD'
) {}
public function getSubunitFactor(): int
{
return match (strtoupper($this->currency)) {
'JPY', 'KRW', 'UGX', 'VND' => 1,
'KWD', 'BHD', 'OMR' => 1000,
default => 100,
};
}
public function toMajorUnit(): float
{
return $this->amount / $this->getSubunitFactor();
}
public function add(Money $other): self
{
$this->assertSameCurrency($other);
return new self($this->amount + $other->amount, $this->currency);
}
public function subtract(Money $other): self
{
$this->assertSameCurrency($other);
return new self($this->amount - $other->amount, $this->currency);
}
public function format(string $locale = 'en'): string
{
return Number::currency($this->toMajorUnit(), $this->currency, $locale);
}
private function assertSameCurrency(Money $other): void
{
if ($this->currency !== $other->currency) {
throw new InvalidArgumentException("Cannot perform arithmetic on different currencies ({$this->currency} vs {$other->currency}).");
}
}
}Next, attach this object to your Eloquent models using Eloquent Custom Casts. Implement IlluminatedContractsdDatabasedEloquentdCastsAttributes so Eloquent handles serialization and deserialization automatically.
namespace App\Casts;
use App\ValueObjects\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
class MoneyCast implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): Money
{
$currency = $attributes['currency'] ?? 'USD';
return new Money((int) $value, $currency);
}
public function set(Model $model, string $key, mixed $value, array $attributes): array
{
if ($value instanceof Money) {
return [
$key => $value->amount,
'currency' => $value->currency,
];
}
if (is_int($value)) {
return [
$key => $value,
];
}
throw new InvalidArgumentException('The price attribute must be an instance of Money or an integer.');
}
}Now bind it to your Eloquent model. You can write `$order->amount` directly, get back a domain object, and do direct calculations without thinking about low-level integers inside controllers.
class Order extends Model
{
protected function casts(): array
{
return [
'amount' => MoneyCast::class,
];
}
}Formatting for Multi-Currency Applications
Laravel 12 comes with built-in locale formatting helpers via the IlluminatedSupportdNumber class. Under the hood, it uses PHP's native NumberFormatter class from the intl extension.
When rendering output for frontends or PDF invoices, pass both amount and currency into Number::currency(). It automatically formats target symbols, placement rules, and decimal counts according to regional standards.
Number::currency(19.99, 'USD', 'en')outputs$19.99Number::currency(19.99, 'EUR', 'fr')outputs19,99 €Number::currency(2500, 'JPY', 'ja')outputs¥2,500Number::currency(12.345, 'KWD', 'ar')outputs12.345 د.ك.
Frontend API Serialization: Next.js 16 and React 19
When serving data to Next.js 16 or React 19 Server Components over a JSON REST or GraphQL API, never send raw floating-point numbers. Floating-point behavior in JavaScript client engines is notoriously flawed: 0.1 + 0.2 equals 0.30000000000000004. If your client side aggregates line items using JS floats, you will render inaccurate subtotals on user shopping carts.
Instead, return both raw integer minor units and pre-formatted display strings inside Eloquent API Resources.
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class OrderResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'amount_raw' => $this->amount->amount,
'currency' => $this->amount->currency,
'formatted_amount' => $this->amount->format($request->user()->locale ?? 'en'),
];
}
}On the Next.js side, display formatted_amount directly on UI cards. If users mutate cart quantities inside React state, recalculate minor units using plain integer math before sending updates back to your Laravel backend.
Tax Calculations and Percentage Discounts
Simple addition and subtraction work cleanly with integer minor units. Division and tax percentages introduce fractional cents. A 8.25% sales tax on an order of $19.99 (1999 cents) equals 164.9175 cents.
You cannot store 164.9175 as an integer. You must explicitly define your rounding policy before saving back to the database. PHP offers three primary rounding modes inside round():
PHP_ROUND_HALF_UP: Rounds 164.5 to 165. Standard commercial rounding.PHP_ROUND_HALF_DOWN: Rounds 164.5 to 164.PHP_ROUND_HALF_EVEN: Rounds to the nearest even number (Banker's rounding). This reduces statistical bias over millions of order aggregates.
Pick one strategy, document it in your team guidelines, and enforce it inside domain actions. For enterprise applications with complex split payments, revenue sharing, or dynamic tax matrices, consider wrapping core math operations in brick/money (`composer require brick/money`), which relies on BCMath or GMP extensions to execute exact-precision arithmetic without floating point leakage.












