PHP: How do you convert a string to a number?
There is a cast for the conversion, and it works. The real question is usually a different one though: what should happen when the string is not a number at all? The cast answers that silently, and rarely in a way you can use.
The casts at a glance
input (int) (float) is_numeric
'42' 42 42 true
'42.7' 42 42.7 true
' 42 ' 42 42 true
'42abc' 42 42 false
'abc' 0 0 false
'' 0 0 false
'1e3' 1000 1000 true
'0x1A' 0 0 false
'007' 7 7 true
'4,99' 4 4 false
'-3.5' -3 -3.5 true
'+7' 7 7 true
Two of those rows are worth remembering. 'abc' becomes 0 — not null, not
false, but a perfectly valid-looking number. And '4,99' becomes 4, which
is probably the most common case of all in a German-language form.
Neither produces a message. Your code simply carries on calculating with a number that has nothing to do with the input.
The cast stays silent, the arithmetic speaks up
This is a difference worth knowing:
(int) '42abc' : 42
'42abc' + 1 : 43 Warning: A non-numeric value encountered
(int) 'abc' : 0
'abc' + 1 : TypeError: Unsupported operand types: string + int
The cast says nothing in any of these cases. The arithmetic, on the other hand,
warns for a partially numeric string — and throws a TypeError for a
completely non-numeric one.
The TypeError is new in PHP 8. In PHP 7 that was only a warning. So if you
have $s + 0 sitting in your code as a conversion, since PHP 8 you have a
potential crash there. It is one of those changes that is easy to miss during
an upgrade, because it only strikes on invalid input.
What I recommend instead
When the input comes from outside — and it almost always does, otherwise you would already have a number — you do not want to cast, you want to check:
<?php
$quantity = filter_var($_POST['quantity'] ?? '', FILTER_VALIDATE_INT);
if ($quantity === false) {
// input was not a valid integer
}
?>
'42' INT: 42 FLOAT: 42.0
'42.7' INT: false FLOAT: 42.7
'42abc' INT: false FLOAT: false
' 42 ' INT: 42 FLOAT: 42.0
'' INT: false FLOAT: false
'0' INT: 0 FLOAT: 0.0
'-3' INT: -3 FLOAT: -3.0
filter_var() returns false for invalid input — that is, something
distinguishable from every valid number. That is exactly what the cast cannot
do.
Note that FILTER_VALIDATE_INT rejects the string '42.7'. That is usually
what you want: somebody expecting a whole number does not want a silently
rounded one.
If you prefer a default value to false:
<?php
$quantity = filter_var($input, FILTER_VALIDATE_INT,
['options' => ['default' => 1]]);
?>
The classic with zero
Watch out when checking:
filter_var('0', FILTER_VALIDATE_INT) : 0
if ($r) : false <- 0 is falsy!
if ($r !== false) : true
'0' is valid input and returns 0. And 0 is falsy. Anyone checking with
if (!$quantity) treats the valid input 0 as an error.
So use === false or !== false. The same trap as with array_search() and
strpos().
The comma
In German-language forms every other user types 4,99. What PHP makes of it:
(float) '4,99' : 4 <- everything after the comma gone
is_numeric('4,99') : false
str_replace(',', '.', ...) first : 4.99
4,99 € becomes four euros. Nobody notices, because 4 is a perfectly
plausible number — until somebody checks the totals.
A str_replace(',', '.', $input) before the conversion clears that up. For
amounts with thousands separators (1.234,56) you also have to strip the dots,
in this order:
<?php
$raw = str_replace(['.', ','], ['', '.'], $input);
$amount = filter_var($raw, FILTER_VALIDATE_FLOAT);
?>
'1.234,56' -> '1234.56' -> 1234.56
'4,99' -> '4.99' -> 4.99
'1234.56' -> '123456' -> 123456.0 <- wrong
'1,234.56' -> '1.23456' -> 1.23456 <- wrong
The bottom two rows show the limit of this approach: it assumes German
notation. If somebody enters English-formatted input, it silently turns into
something else entirely — 1234.56 becomes 123,456.
If you cannot rule that out, use NumberFormatter from the intl extension.
It knows the conventions of the respective language:
<?php
$nf = new NumberFormatter('de_DE', NumberFormatter::DECIMAL);
$amount = $nf->parse('1.234,56'); // 1234.56
?>
That does not make the problem go away entirely — you still have to know which
language the user typed in. It is more reliable not to allow both notations in
the form to begin with: an <input type="number"> always sends the server a
dot as the decimal separator, regardless of what the user sees in the browser.
intval() with a base
This is something the cast cannot do:
intval('0x1A') : 0
intval('0x1A', 16) : 26
intval('1A', 16) : 26
intval('012', 8) : 10
intval('101', 2) : 5
intval('0x1A', 0) : 26
Base 0 means “look at the prefix”: 0x hexadecimal, a leading 0 octal,
otherwise decimal. Useful when reading configuration files in which file
permissions are written as 0644.
Where the numbers stop
Two limits worth knowing.
The size:
PHP_INT_MAX : 9223372036854775807
(int) '9223372036854775808' : 9223372036854775807 <- silently capped
'9223372036854775808' + 0 : float
The cast silently caps at PHP_INT_MAX. So with an ID from a foreign system
you get a wrong but plausible-looking number. Such IDs are better left as
strings — you do not do arithmetic with them anyway.
The precision:
0.1 + 0.2 == 0.3 : false
0.1 + 0.2 : 0.30000000000000004441
(int) ((0.1 + 0.7) * 10) : 7
The last line is the classic from the PHP manual. (0.1 + 0.7) * 10 is
internally marginally less than 8, and (int) truncates rather than rounds.
Out comes 7.
For monetary amounts that means: calculate in cents, so with whole numbers. Divide by 100 only on output. That spares you a whole class of bugs which otherwise surface eventually as “one cent difference” in the accounts.
is_numeric or ctype_digit?
Finally the difference that often gets overlooked:
'42' is_numeric: true ctype_digit: true
'4.2' is_numeric: true ctype_digit: false
'-4' is_numeric: true ctype_digit: false
' 42' is_numeric: true ctype_digit: false
'42 ' is_numeric: true ctype_digit: false
'1e3' is_numeric: true ctype_digit: false
'' is_numeric: false ctype_digit: false
is_numeric() accepts a sign, a decimal point, exponential notation and
whitespace both before and after. If you mean “digits only” — for a postcode or
a customer number — use ctype_digit().
And note that ctype_digit('') also returns false, so an empty string does
not slip through.
Summary
- For input from outside use
filter_var()rather than a cast — it can tell “invalid” from “a valid zero”. - Check with
=== false, not for falsiness. (int) 'abc'is0, without any message at all.'abc' + 1is aTypeErrorsince PHP 8, no longer a warning.(float) '4,99'is4— rewrite German-formatted input first.- Large numbers are silently capped at
PHP_INT_MAXby the cast. - Calculate monetary amounts in cents, not in euros.
ctype_digit()when you really do mean digits only.
About Netcup (advertisement)
The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).