Data Types And Standard Library
Numbers, Floating-Point Precision, And Money
PHP mainly gives you integers and floating-point numbers. Integers are exact whole numbers. Floats are approximate decimal numbers. That difference matters.
Integers
<?php
declare(strict_types=1);
$quantity = 3;
$unitPricePennies = 1299;
$lineTotalPennies = $quantity * $unitPricePennies;
echo $lineTotalPennies;
// Prints:
// 3897
Integers are the right choice for counts, quantities, and money stored in the smallest unit, such as pennies or cents.
Money should usually use integer minor units
Do calculations in integer pennies, then format for display at the boundary.
<?php
declare(strict_types=1);
$priceInPennies = 1299;
$quantity = 3;
$totalInPennies = $priceInPennies * $quantity;
echo 'GBP ' . number_format($totalInPennies / 100, 2) . PHP_EOL;
// Prints:
// GBP 38.97
This is safer than doing financial calculations with floating-point pounds throughout the program.
Floats are approximate
Floats are useful for measurements and ratios, but they are not exact decimal money values.
<?php
declare(strict_types=1);
var_dump(0.1 + 0.2);
// Output is a float close to 0.3, not a decimal-money guarantee.
Do not compare important float calculations as if they were exact strings or exact money values. If exact decimal arithmetic is required, use integer minor units or a decimal/math library designed for that job.
Division
The / operator returns a float. Use intdiv() when you need integer division.
<?php
declare(strict_types=1);
var_dump(5 / 2);
var_dump(intdiv(5, 2));
// Prints:
// float(2.5)
// int(2)
Guard against division by zero before calling / or intdiv().
<?php
declare(strict_types=1);
function averagePennies(int $totalPennies, int $count): int
{
if ($count === 0) {
throw new InvalidArgumentException('Count must be greater than zero.');
}
return intdiv($totalPennies, $count);
}
echo averagePennies(1000, 4);
// Prints:
// 250
Rounding
Rounding should happen deliberately, close to the rule that requires it.
<?php
declare(strict_types=1);
$average = 10 / 3;
echo number_format($average, 2);
// Prints:
// 3.33
number_format() is for display. Do not confuse display formatting with internal calculation.
Numeric strings from input
Form and query-string input usually arrives as strings. Validate and convert at the boundary.
<?php
declare(strict_types=1);
function parseQuantity(string $value): int
{
if (! ctype_digit($value)) {
throw new InvalidArgumentException('Quantity must be a whole number.');
}
$quantity = (int) $value;
if ($quantity < 1) {
throw new InvalidArgumentException('Quantity must be at least 1.');
}
return $quantity;
}
echo parseQuantity('3');
// Prints:
// 3
Do not let unvalidated strings drift deep into calculation code.
Overflow and large values
PHP integers have limits based on the platform. Most web application values will not approach those limits, but imports, IDs, timestamps, and financial aggregates can become large.
Use clear names, validate ranges, and be careful when multiplying large values.
What to remember
Use integers for counts and money in minor units. Use floats for approximate measurements. Validate numeric strings at the boundary. Guard division by zero. Format numbers for display only at the output boundary.
Before moving on, make sure you can explain why 1299 pennies is safer for calculations than 12.99 pounds.
Model The Quantity Before Choosing A Type
A number in source code is not yet a domain model. The value 20 might mean twenty items, twenty cents, a twenty-percent discount, a page size, or an identifier that should never participate in arithmetic. Give units a visible place in names and APIs: $quantity, $amountInCents, $discountBasisPoints, and $timeoutMilliseconds communicate operations that a bare $value hides.
Integers are exact within their supported range, which makes them a strong representation for counts and minor-unit currency amounts. Exact does not mean unlimited. Multiplication can overflow when an import contains extreme quantities or when many rows are aggregated. Validate realistic upper bounds before arithmetic, and use a representation such as BCMath when valid values can exceed the platform integer range.
Floats model approximate binary quantities. They are appropriate for measurements whose uncertainty already exceeds tiny representation errors, such as a temperature displayed to one decimal place. They are a poor default for equality-sensitive values. Decimal fractions such as 0.1 usually have no finite binary representation, so a sequence of apparently simple operations can produce a nearby value rather than the decimal result printed on paper.
Money Needs Amount And Currency
Storing 1,099 as an integer is useful only when the application also knows whether it means GBP 10.99, USD 10.99, or a currency with another minor-unit convention. A money value should carry both amount and currency and should prevent arithmetic between unlike currencies unless an explicit exchange operation is performed.
Do not assume that every currency has two decimal places. Payment providers and financial libraries publish the exponent they expect for each supported currency. Keep that policy in one reviewed mapping or library rather than scattering division by 100 through templates and controllers. Provider APIs may also impose minimum and maximum amounts that are narrower than PHP integer limits.
Parsing is part of the model. A browser submits text, not a trustworthy decimal. Validate the accepted grammar before conversion: decide whether signs, grouping separators, surrounding whitespace, and more fractional digits than the currency supports are allowed. Converting the text to float first defeats the reason for using integer minor units because approximation occurs before the integer is produced.
Rounding And Allocation Are Business Rules
Tax, discounts, exchange rates, and proportional refunds often create fractions of the smallest payable unit. The application must define where rounding happens and which mode applies. Rounding each line and then summing can differ from computing an invoice total and rounding once. Neither result is universally correct; tax authorities, contracts, and providers may prescribe one.
Allocation needs an additional conservation rule. If 1,000 cents must be split equally across three recipients, integer division produces 333 cents each and leaves one cent. A deterministic allocator distributes the remainder according to a documented order so that the allocated values always sum to the original total. Never discard the remainder or let iteration order decide who receives it.
Useful invariants include:
- every accepted amount has a known currency and unit;
- the sum of allocated parts equals the source amount;
- refunds never exceed the refundable balance;
- a displayed value parses back to the same stored amount when round trips are promised;
- intermediate operations use the documented rounding point and mode;
- values outside operational limits are rejected before arithmetic.
Percentages, Rates, And Ratios
A percentage is another value with a scale. Storing 12.5 as a float may be adequate for display, but financial calculations often use basis points or decimal strings. One basis point is one hundredth of a percentage point, so 12.50 percent can be represented as 1,250 basis points. Names should include the unit because multiplying an amount by 1250 is meaningless without the scale.
Rates may produce values that cannot be represented exactly in the target unit. Compute with enough intermediate precision, then round at the boundary defined by the business rule. Avoid repeatedly formatting and reparsing values; formatted strings may contain localized separators and are presentation output, not calculation input.
Numeric Strings At Application Boundaries
Forms, JSON payloads, CSV imports, database drivers, and environment variables can all provide numeric-looking strings. PHP coercion rules are convenient but can blur invalid input. Validate according to the domain before casting. Product IDs might permit only ASCII digits without a sign. Decimal amounts may require a fixed number of fractional digits. Scientific notation may be valid for measurements but forbidden for currency entry.
Loose comparison is especially risky around numeric strings because PHP may compare after conversion. Prefer explicit validation and strict comparison. Once a value crosses the boundary, represent it consistently so downstream code does not repeatedly ask whether it is currently an integer, float, or string.
Testing Numeric Code
Example-based tests should cover zero, one smallest unit, negative values where allowed, values around every rounding boundary, and the largest supported input. Include currencies with different minor-unit policies if the application is multi-currency. Test malformed text such as repeated signs, grouping separators in the wrong location, exponent notation, empty strings, and excessive fractional digits.
Property tests are valuable for arithmetic invariants. Generate many valid totals and allocation counts, then assert that all parts are integers, no forbidden negative part appears, and the parts sum to the original. For percentage calculations, compare the implementation with independently prepared examples from the governing specification or payment-provider sandbox.
Integration tests should verify how the database driver returns numeric columns and how JSON encoding represents values. A schema migration from decimal text to integer minor units needs fixtures from the old representation, a checked conversion, reconciliation totals before and after migration, and a rollback plan that does not reinterpret already converted data.
Review Checklist
Before approving numeric code, identify the quantity, unit, valid range, precision, and source of the value. Confirm the exact point where parsing, rounding, allocation, formatting, and unit conversion occur. Search for casts from float to integer, arithmetic on formatted strings, unnamed scale factors, and comparisons between mixed numeric types.
Database And API Representation
Match storage types to the application contract. Integer minor units map naturally to integer columns, while exact decimals may use a database decimal type with an explicit precision and scale. Verify how the PHP driver returns that column: some drivers return decimal values as strings to preserve precision. Casting those strings to float in a repository silently changes the contract.
JSON has only one general number syntax, and consumers may parse large integers through a floating-point type. Identifiers and exact values beyond the safe integer range of browser JavaScript may need string representation in an API. Document that choice in the schema and keep it stable. A value changing from JSON number to string can break generated clients even when the printed digits look identical.
During migrations, reconcile totals rather than checking only row counts. Compare sums grouped by currency, inspect values around rounding boundaries, and retain the original column until the converted representation has been verified.
After this lesson, you should be able to choose between integers, floats, and decimal arithmetic based on the quantity being represented; model money with currency and minor-unit policy; define rounding and allocation explicitly; validate numeric strings at boundaries; and test exact arithmetic with examples and invariants.
Practice
Task: Calculate a line total
Write a small price calculator that keeps money as integer pennies.
Requirements
- Use strict types.
- Create a
lineTotalPennies()function. - Accept unit price in pennies and quantity as integers.
- Reject quantities lower than
1. - Return the line total as integer pennies.
- Format the final result for display as
GBP 38.97. - Include one normal case and one invalid quantity case.
Show solution
<?php
declare(strict_types=1);
function lineTotalPennies(int $unitPricePennies, int $quantity): int
{
if ($quantity < 1) {
throw new InvalidArgumentException('Quantity must be at least 1.');
}
return $unitPricePennies * $quantity;
}
function formatMoney(int $pennies): string
{
return 'GBP ' . number_format($pennies / 100, 2);
}
echo formatMoney(lineTotalPennies(1299, 3)) . PHP_EOL;
try {
lineTotalPennies(1299, 0);
} catch (InvalidArgumentException $exception) {
echo $exception->getMessage() . PHP_EOL;
}
// Prints:
// GBP 38.97
// Quantity must be at least 1.
The calculation uses integer pennies. Formatting happens only when printing the result.