Avoid Floating‑Point Errors in PHP Money Calculations with TypePHP’s Decimal Types
This article explains how PHP's floating‑point precision pitfalls can be eliminated by using TypePHP's built‑in high‑precision types—Decimal, BigInt, and BigFloat—through a complete order‑total example, detailed type‑system overview, inference rules, conversion methods, and performance‑critical tips.
01. Practical Example: Order Amount Calculator
First, the full code for order.php demonstrates calculating an order total where price, quantity, discount, and tax are handled without losing precision. The file declares strict_types=1 and uses decimal_types, so all floating‑point literals become Decimal objects.
<?php
// order.php
declare(strict_types=1);
use decimal_types; // all float literals become Decimal
function main(): void {
$price = 19.9; // Decimal, not float!
$qty = 3;
$discount = 0.85; // Decimal
$total = $price * $qty * $discount;
$tax = $total * 0.06;
echo "小计: " . $total->toString() . "
";
echo "税费: " . $tax->toString() . "
";
echo "应付: " . ($total + $tax)->toString() . "
";
}Compile and run:
tpc order.php -O2 -o order
./orderExpected output:
小计: 50.745
税费: 3.0447
应付: 53.7897Key point: the line use decimal_types makes every float literal a Decimal , eliminating binary floating‑point errors. Running the same logic with native PHP would produce a trailing "53.78970000000001".
02. Overview of TypePHP Types
TypePHP’s compiler maintains a set of C++ storage types (php::*) that map to PHP types. Important entries include: php::Var → mixed (dynamic type, default behavior) php::Int / php::Float / php::Bool → int / float / bool (native types, require use native_types) php::Str / php::Array → string /
array php::Object→
object php::Stream→ resource (TypePHP‑specific stream) php::BigInt / php::Decimal / php::BigFloat → high‑precision trio (BigInt, Decimal, BigFloat) php::StdVector / php::StdMap → C++ standard library containers
Note: without any use statements, all variables default to php::Var , preserving PHP’s dynamic nature. Optimizations rely on explicit type declarations.
03. Three Declaration Traps that Revert to Dynamic Type
While most PHP type declarations map directly to static types, three declarations always degrade to php::Var: null →
php::Var callable→ php::Var (compiler cannot track closures and callbacks) iterable → php::Var Hot‑path functions using any of these lose native‑call optimizations. Example of a degrading declaration:
<?php
use native_types;
// Bad: callable parameter forces dynamic handling
function apply(callable $fn, int $x): int {
return $fn($x); // dynamic call, no optimization
}
// Good: pure int parameter retains static type
function square(int $x): int {
return $x * $x;
}04. Type Inference Rules
During compilation, TypePHP automatically promotes literals:
Integer literals with more than 19 digits become BigInt.
Floating‑point literals with more than 16 significant digits become Decimal.
$id = 12345678901234567890; // 19‑digit, auto BigInt
$pi = 0.1234567890123456; // 16‑digit, auto DecimalBinary‑operation precedence (high to low): BigFloat > Decimal > BigInt > Float > Int. If any operand is of a higher‑precision type, the result is promoted, except that integer division remains Int and falls back to Var when not exact.
Strict rule: a Float variable cannot be implicitly converted to Decimal; it must be constructed from a string to avoid hidden precision loss.
$a = std::bigInt("12345678901234567890"); // string construction, recommended
$b = std::decimal("0.01");
$c = std::bigFloat("1.2345e100");05. to* Methods for Explicit Conversion
TypePHP provides a family of to* methods that are chainable and allow the compiler to infer exact return types, surpassing PHP’s cast operators.
declare(strict_types=1);
use native_types;
function convert_demo(mixed $input): void {
$i = $input->toInt(); // equivalent to (int)$input
$f = $input->toFloat();
$s = $input->toString();
$b = $input->toBool();
$a = $input->toArray();
}The toArray() method first checks whether the object defines its own toArray() method; if not, it falls back to converting public properties.
class User {
public int $id;
public string $name;
public function toArray(): array {
return ['uid' => $this->id, 'display_name' => $this->name];
}
}
$user = new User(1, 'admin');
$arr = $user->toArray(); // ['uid' => 1, 'display_name' => 'admin']06. Re‑attaching Types after Mixed Returns
When a value extracted from an array or a resource is typed as mixed, subsequent method calls become dynamic. Using toObject() or toStream() restores a static type, enabling native calls and improving performance.
$user = $array['user']->toObject(User::class);
echo $user->greet(); // compiler now knows $user is a User
$sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0);
$client = $sockets[0]->toStream();
$client->write("hello");In hot loops, adding the toObject() / toStream() line can make a noticeable difference, as discussed in the second article about "Native Call Conditions".
07. any() – Opt‑out of Type Tracking
The compiler infers a variable’s type from its first assignment; later assignments of a different type cause an error. When a variable truly needs to hold multiple possible types, any() disables compile‑time tracking.
function main(): void {
$rand = random_int(0, 10000);
if ($rand % 2) {
$o = any(new Foo1());
} else {
$o = any(new Foo2()); // without any() this would be a type conflict
}
if (method_exists($o, 'run')) {
$o->run(); // dynamic dispatch
}
} any()incurs no runtime check overhead, but the variable becomes Var, losing native‑call optimizations. The performance‑flexibility trade‑off is explicit.
08. Summary
Use use decimal_types for monetary calculations; use use bigint_types for extremely large integers. Literals automatically upgrade to high‑precision types.
High‑precision types are immutable; each operation returns a new value. Float cannot be implicitly turned into Decimal; string construction is required.
Declarations of null, callable, and iterable degrade to dynamic type; avoid them in performance‑critical code. toObject() / toStream() reconnect types extracted from arrays or resources, a key performance detail.
When dynamic behavior is truly needed, use any(), but be aware of the loss of native‑call benefits.
The next article will explore compile‑time annotations such as NotNull, NotEmpty, and Validate, showing how they can eliminate null‑pointer and illegal‑argument errors at compile time.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Open Source Tech Hub
Sharing cutting-edge internet technologies and practical AI resources.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
