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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 example
<?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 unitPricePennies and quantity.
  • 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 example
<?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.