data types and standard library
Math, BCMath, and GMP
The professional skill is choosing the right numeric representation. Prices, percentages, tax, balances, identifiers, counters, and cryptographic-sized integers do not all want the same tool.
Use integers for money in minor units
For normal application money, store and calculate in minor units such as pennies or cents.
<?php
declare(strict_types=1);
$unitPricePennies = 1299;
$quantity = 3;
$totalPennies = $unitPricePennies * $quantity;
echo 'Total pennies: ' . $totalPennies . PHP_EOL;
echo 'Display: £' . number_format($totalPennies / 100, 2) . PHP_EOL;
// Prints:
// Total pennies: 3897
// Display: £38.97
This avoids floating-point surprises while keeping the data easy to store, compare, and sum.
Know where floats are acceptable
Floats are approximate. They are fine for measurements, charts, averages, and non-financial calculations, but they are risky for exact money rules.
<?php
declare(strict_types=1);
$result = 0.1 + 0.2;
echo number_format($result, 17) . PHP_EOL;
// Prints:
// 0.30000000000000004
This does not mean floats are broken. It means they represent many decimal values approximately.
Use BCMath for decimal strings
BCMath works with decimal numbers as strings and lets you choose the scale.
<?php
declare(strict_types=1);
$total = bcadd('10.25', '2.40', 2);
$tax = bcmul($total, '0.20', 2);
echo $total . PHP_EOL;
echo $tax . PHP_EOL;
// Prints:
// 12.65
// 2.53
BCMath is useful when a domain requires decimal arithmetic beyond ordinary integer minor units. The inputs and outputs are strings, so keep that explicit in function names and PHPDoc.
Validate decimal strings before using BCMath
BCMath expects numeric strings. Validate input at the boundary.
<?php
declare(strict_types=1);
function requireDecimalString(string $value): string
{
if (!preg_match('/^\d+(\.\d{1,2})?$/', $value)) {
throw new InvalidArgumentException('Expected a decimal with up to two places.');
}
return $value;
}
echo bcadd(requireDecimalString('10.25'), requireDecimalString('2.40'), 2) . PHP_EOL;
// Prints:
// 12.65
Do not pass raw request values straight into numeric calculations.
Use GMP for large integers
GMP is for integers larger than PHP's normal integer range or for number-theory style operations.
<?php
declare(strict_types=1);
$largeProduct = gmp_strval(gmp_mul('123456789123456789', '9'));
echo $largeProduct . PHP_EOL;
// Prints:
// 1111111102111111101
GMP values are not normal integers. Convert them deliberately when outputting or storing.
Check extension availability
BCMath and GMP may not be installed everywhere.
<?php
declare(strict_types=1);
echo extension_loaded('bcmath') ? 'BCMath available' : 'BCMath missing';
echo PHP_EOL;
echo extension_loaded('gmp') ? 'GMP available' : 'GMP missing';
echo PHP_EOL;
If a project needs one of these extensions, put it in environment documentation, deployment checks, and CI images.
Rounding is a business rule
Rounding mode and timing matter. Round at the wrong point and totals can disagree with invoices, reports, or payment providers.
<?php
declare(strict_types=1);
$average = 10 / 3;
echo round($average, 2) . PHP_EOL;
// Prints:
// 3.33
For money, decide whether rounding happens per line item, per tax rate, per invoice, or only at display time.
What to remember
Use integers for ordinary money, floats for approximate measurement, BCMath for decimal string arithmetic, and GMP for very large integers. Validate numeric input, document required extensions, and treat rounding as a business decision rather than a formatting detail.
Practice
Task: Calculate an order total
Write a small order total calculator that uses integer minor units.
Requirements
- Use
declare(strict_types=1);. - Accept line items with
unitPricePenniesandquantity. - Reject negative prices.
- Reject quantities less than
1. - Calculate the total in pennies as an integer.
- Format the total for display.
- Print one valid total.
- Show one invalid line item by catching the exception.
- Include the expected output as comments in the same PHP code block.
The task should avoid floats for the core money calculation.
Show solution
<?php
declare(strict_types=1);
function orderTotalPennies(array $lines): int
{
$total = 0;
foreach ($lines as $line) {
$unitPrice = $line['unitPricePennies'] ?? null;
$quantity = $line['quantity'] ?? null;
if (!is_int($unitPrice) || $unitPrice < 0) {
throw new InvalidArgumentException('Unit price must be a non-negative integer.');
}
if (!is_int($quantity) || $quantity < 1) {
throw new InvalidArgumentException('Quantity must be at least 1.');
}
$total += $unitPrice * $quantity;
}
return $total;
}
$totalPennies = orderTotalPennies([
['unitPricePennies' => 1299, 'quantity' => 3],
['unitPricePennies' => 399, 'quantity' => 2],
]);
echo $totalPennies . PHP_EOL;
echo '£' . number_format($totalPennies / 100, 2) . PHP_EOL;
try {
orderTotalPennies([
['unitPricePennies' => 1299, 'quantity' => 0],
]);
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
// Prints:
// 4695
// £46.95
// Quantity must be at least 1.
The core calculation stays in integer pennies. Formatting to pounds only happens at the output boundary.