Keep invoice amounts out of floating-point arithmetic
An invoice total should not depend on the display rounding of a binary floating-point result. Values such as 0.1 cannot be represented exactly as binary floats. A total that looks fine after formatting may still compare differently or accumulate small errors across many lines.
Use an explicit unit
For a currency with two fractional digits, integer minor units make a small calculation straightforward:
<?php
$unitPriceMinor = 1999;
$quantity = 3;
$totalMinor = $unitPriceMinor * $quantity;
assert($totalMinor === 5997);
$display = intdiv($totalMinor, 100).'.'.str_pad((string) ($totalMinor % 100), 2, '0', STR_PAD_LEFT);
assert($display === '59.97');
This display example handles non-negative amounts in one two-decimal currency. Refunds, currencies with other exponents, and large values need their own rules. Check integer overflow limits for the quantities your application accepts.
Rounding is a business decision
Tax calculated per line can differ from tax calculated on an invoice subtotal. Decide the required rounding mode and where rounding occurs, then document it with examples agreed with the business. A decimal arithmetic library can preserve decimal calculations; it cannot decide the correct commercial rule.
At the database boundary, use an appropriate exact representation, such as integer minor units or DECIMAL. Do not cast a DECIMAL string to float just to pass it through a DTO. Test discounts, partial refunds, multiple currencies, and totals near your accepted maximum. Keep the currency alongside the amount so two unrelated units cannot be silently added.
Reference
Official documentation. The examples here illustrate the stated boundary; adapt them to your application and test its failure paths.