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.
Choose The Library From The Number System
BCMath and GMP solve different problems. BCMath operates on decimal numbers represented as strings. It is useful when decimal fractions must remain exact to a chosen scale, such as interest calculations or intermediate monetary values. GMP operates on integers of effectively arbitrary size and provides number-theory operations. It is useful for very large counters, cryptographic exercises, combinatorics, and integer algorithms that exceed PHP integer range.
Neither extension makes a calculation correct by itself. The application still owns units, accepted syntax, rounding, maximum input size, and the meaning of a result. An exact calculation with the wrong scale or formula is still wrong.
BCMath Uses Decimal Strings
BCMath functions accept numeric strings and return strings. Keep the value in that form throughout the exact-decimal calculation. Converting user input to float and then passing the float to bcadd() has already introduced binary approximation. Validate the original text and pass the validated decimal string directly.
A strict decimal grammar might accept 0, 12, 12.50, and -0.25 while rejecting grouping separators, exponent notation, whitespace, and more fractional digits than the use case permits. The grammar is a product decision. A scientific application may intentionally accept exponent notation through another parser, while a checkout amount normally should not.
BCMath operations need a scale: the number of fractional decimal digits retained in the result. Relying on global bcscale() can make library behavior depend on process setup or another caller. Passing an explicit scale to each operation makes the calculation easier to review. Use a larger intermediate scale when multiplication or division needs guard digits, then apply the required rounding once at the documented boundary.
BCMath historically truncates results to the requested scale rather than applying a business rounding rule. Do not assume truncation is equivalent to round-half-up, bankers rounding, or a statutory tax rule. Implement or use a tested decimal rounding operation and include negative values because tie behavior around zero is often misunderstood.
GMP Is For Large Integers
GMP values are objects backed by a native arbitrary-precision integer library. Construct them from validated integer strings when input may exceed PHP integer range. Casting an oversized string to int before calling gmp_init() can lose information.
GMP supports arithmetic, comparisons, powers, greatest common divisors, modular arithmetic, primality functions, and bit operations. These capabilities are useful, but cryptographic protocol implementation should normally use established high-level cryptography libraries. Correct algorithms also require constant-time behavior, secure randomness, parameter validation, and side-channel resistance that a few GMP calls do not provide.
Large inputs can consume significant CPU and memory. Put limits on digit counts, exponents, and operation types when users control them. A request that computes an enormous power or repeatedly tests huge candidate primes can become a denial-of-service vector even though each input is syntactically valid.
Extension Availability Is A Deployment Contract
BCMath and GMP are optional PHP extensions. The CLI, PHP-FPM, queue worker, and test process may load different php.ini files or even different PHP builds. Checking extension_loaded() in one shell does not prove that the web process has the same capability.
Declare required extensions in Composer using ext-bcmath or ext-gmp. That catches many missing environments during dependency installation, but production images still need the corresponding operating-system packages and native libraries. Add a startup or health diagnostic that reports the PHP version, SAPI, loaded extension, and relevant library version without exposing secrets.
After installing or upgrading an extension, restart long-running workers and PHP-FPM processes. Existing processes do not acquire a newly installed module. Build immutable deployment images where possible so staging and production run the same compiled extension set.
Designing A Decimal Calculation
Take a loan-interest calculation as an example. Name the units of principal, annual rate, periods, and result. Decide whether the rate is a percentage string, basis points, or a decimal ratio. Define the intermediate scale and the final rounding policy. Then write the formula so every conversion is visible.
Do not mix a BCMath decimal string with ordinary arithmetic operators. PHP may coerce it to an integer or float. Wrap repeated decimal operations in a small application-owned value type or calculation service that fixes scale and validates operands. The wrapper should express the business operation, not merely rename every BCMath function.
For money, integer minor units are still often simpler for stored and payable amounts. BCMath is valuable when a formula needs exact fractional intermediates before producing the final minor-unit integer. Convert at one reviewed boundary and verify that the final amount falls within supported integer and provider ranges.
Failure Handling
Different failures need different responses. A malformed decimal is a validation error. Division by zero is usually invalid domain input or a programming defect. A missing extension is a deployment failure that should stop startup, not a condition to hide by falling back to float. An input that exceeds a documented size limit should be rejected before expensive native work begins.
Log the operation name, scale, input sizes, and error category, but do not automatically log sensitive financial operands. A stack trace from an extension function is useful to developers; a public API should translate it into an application-owned error without exposing filesystem paths or implementation details.
Verification
Prepare examples independently of the implementation. For decimal arithmetic, include repeating divisions, values just below and above rounding ties, negative values, and calculations with many intermediate operations. Assert exact strings where scale is part of the contract. Compare financial examples with an authoritative spreadsheet, provider example, or governing formula.
For GMP, test values immediately below and above PHP_INT_MAX, negative integers, zero, very long input strings, and rejected non-integer syntax. Add performance tests for the largest allowed input rather than only tiny examples. Where modular arithmetic matters, verify algebraic properties over generated cases.
Run integration checks under every production SAPI and worker image. Confirm Composer platform requirements, extension versions, process restarts, and failure behavior when the module is absent. A test that conditionally skips all assertions when BCMath is missing can make CI green while shipping an unusable application; required capabilities should fail the build clearly.
Keep Calculation Context Reproducible
A calculation should not depend on whichever scale a previous request, package, or bootstrap file selected. Pass scale explicitly or place it in an immutable calculation policy used by the operation. Include the rounding mode and output unit in that policy when they are domain decisions.
Persist source inputs and the policy version when a result may need to be audited or reproduced later. Re-running an old transaction with a new rate table or rounding rule can produce a different answer even though BCMath remains exact. Versioned policies let support staff distinguish a historical calculation from a current quote.
When exchanging decimal values through JSON, send a string if trailing zeros, exact scale, or values beyond a consumer's safe numeric range matter. Validate the receiving contract with another implementation rather than assuming every client preserves the same decimal semantics.
After this lesson, you should be able to select BCMath for exact decimal-string arithmetic and GMP for large integers, validate inputs before native operations, define scale and rounding explicitly, declare extension requirements, and verify calculations and runtime availability across the processes that execute them.
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.