The bcmath extension does exact decimal arithmetic on numeric strings of any size. Its functions (bcadd(), bcsub(), bcmul(), bcdiv(), bcmod(), bcpow(), bcsqrt(), bccomp()) take an explicit $scale and truncate beyond it. PHP 8.4 added bcround(), bcfloor(), bcceil(), bcdivmod() and the immutable BcMath\Number class, with overloaded operators and automatic scale.
<?php
use BcMath\Number;
echo bcadd('0.1', '0.2', 1), ' ', bcmul('123456789012345678901234567890', '3'), ' ',
bcdiv('1', '3', 20), ' ', bcround('2.345', 2), "\n"; // strings, explicit scale
$price = new Number('19.99'); // PHP 8.4 object API
$net = $price * 3;
$vat = ($net * '0.2')->round(2, RoundingMode::HalfEven);
$gross = $net + $vat;
printf("net %s (scale %d) + VAT %s = %s | %s %s | %s\n", $net, $net->scale, $vat, $gross,
var_export($gross > 70, true), $gross->compare('71.96'), new Number('1') / 3);
set_error_handler(fn(int $no, string $msg) => print "E_DEPRECATED ($no): $msg\n");
echo $price + 0.1, "\n"; // a float operand becomes int 00.3 370370367037037036703703703670 0.33333333333333333333 2.35 net 59.97 (scale 2) + VAT 11.99 = 71.96 | true 0 | 0.3333333333 E_DEPRECATED (8192): Implicit conversion from float 0.1 to int loses precision 19.99
Multiplication adds the operands' scales; division uses the dividend's scale plus 10, so pass a scale to div() for more. The trap: a float operand is cast to int, so + 0.1 adds zero, with only an E_DEPRECATED notice that Ubuntu 225 's default error_reporting hides. Build values from strings, and use compare(), since === tests identity. MySQL 524 DECIMAL values arrive through PDO as strings (Special Column Types), ready for new Number(). For big integers gmp is faster; for currencies, see brick/money 1,928 (https://github.com/brick/money 1,928 ).