Type Juggling & Casting
PHP lets you mix types freely. You can add a string to a number, compare a number with a string, or pass '72' to a function that wants an int. Behind the scenes PHP converts the values for you. This automatic conversion is called type juggling, and it is the reason '1' == '01' is true in PHP.
You met the basics in Data Types. In this lesson you will learn exactly when PHP juggles, read a full table of loose and strict comparisons, see what changed in PHP 8, and learn to convert values on purpose with casts, intval() and settype(). You will finish with strict_types, which stops juggling at function boundaries.
When does PHP juggle?
PHP converts a value whenever an operation needs a different type. The most common places are:
- Arithmetic (
+,-,*,/): strings become numbers. - Joining strings with
.: numbers become strings. - Conditions such as
if ($x): any value becomes abool. - Loose comparison with
==: both sides may be converted. - Function arguments, unless
strict_typesis on.
Everything from a form, the URL or a CSV file arrives as a string, so juggling happens all the time in web code:
<?php
declare(strict_types=1);
$qty = '3'; // from a form, so it is a string
$price = 249.5;
var_dump($qty * $price); // string becomes a number
var_dump($qty . ' items'); // stays a string
var_dump('5' + '10'); // two numeric strings, int result
var_dump('1.5' + 1); // float result
var_dump(7 / 2); // int / int can give a float
var_dump(10 / 2); // ... or an intfloat(748.5) string(7) "3 items" int(15) float(2.5) float(3.5) int(5)
Numeric strings
For arithmetic, PHP sorts strings into three groups:
- A numeric string looks exactly like a number:
'42','4.5','4.2e1'. Spaces before or after are allowed (trailing spaces only since PHP 8). - A leading-numeric string starts with a number and then has other text, such as
'250 rupees'. PHP uses the number but shows a warning. - A non-numeric string such as
'abc'cannot be used in arithmetic at all: PHP 8 throws aTypeError.
is_numeric() tells you which strings are fully numeric. Notice that hexadecimal text like '0x1A' is not numeric:
<?php
declare(strict_types=1);
$inputs = ['42', ' 42', '42 ', '4.2e1', '0x1A', '42abc', 'abc', ''];
foreach ($inputs as $value) {
printf("%-8s %s\n", var_export($value, true), is_numeric($value) ? 'numeric' : 'not numeric');
}
$total = '250 rupees' + 50;
var_dump($total);'42' numeric ' 42' numeric '42 ' numeric '4.2e1' numeric '0x1A' not numeric '42abc' not numeric 'abc' not numeric '' not numeric Warning: A non-numeric value encountered in ... on line 10 int(300)
The warning is your friend: it tells you that dirty input reached your maths. Writing 'two hundred' + 50 instead stops the script with TypeError: Unsupported operand types: string + int.
Loose and strict comparison
The loose operator == juggles types before comparing. The strict operator === never converts: different types are simply not equal. These results come from a real PHP 8.4 run:
| Expression | == | === |
|---|---|---|
'0' and 0 | true | false |
'1' and '01' | true | false |
'10' and '1e1' | true | false |
100 and '1e2' | true | false |
42 and ' 42' | true | false |
42 and '42abc' | false | false |
0 and '' | false | false |
0 and 'a' | false | false |
null and false | true | false |
null and '' | true | false |
'0' and false | true | false |
[] and false | true | false |
true and 'false' | true | false |
-1 and true | true | false |
1.0 and 1 | true | false |
Some rows deserve a second look. true == 'false' is true, because the non-empty string 'false' is truthy. Two numeric strings such as '10' and '1e1' are compared as numbers, even though both sides are strings. That is why comparing passwords, tokens or PIN codes with == is a bug: always use ===, or hash_equals() for secrets.
Loose comparison also hides inside other features. in_array() and switch compare loosely, while match compares strictly:
<?php
declare(strict_types=1);
var_dump(in_array('1e1', ['10', '20'])); // loose: true
var_dump(in_array('1e1', ['10', '20'], true)); // strict: false
$code = '0';
switch ($code) {
case false:
echo "switch: matched false\n";
break;
default:
echo "switch: default\n";
}
echo match ($code) {
false => 'match: false',
'0' => 'match: string zero',
default => 'match: default',
}, "\n";bool(true) bool(false) switch: matched false match: string zero
Pass true as the third argument to in_array() and array_search(), and prefer match over switch when types matter.
What changed in PHP 8
PHP 8 changed how a number is compared with a string. If the string is numeric, PHP still compares them as numbers. If the string is not numeric, PHP 8 turns the number into a string and compares two strings. So 0 == 'a' asks "is '0' the same text as 'a'?", and the answer is false.
Casting on purpose
A cast converts a value when you decide to. Write the type in brackets: (int), (float), (string), (bool), (array) or (object). The function forms intval(), floatval(), strval() and boolval() do the same job, and intval() can also read other number bases:
<?php
declare(strict_types=1);
var_dump((int) '1e3'); // scientific notation is read
var_dump((int) 9.99); // cuts off the fraction
var_dump((int) round(9.99)); // round first if you want 10
var_dump((string) false); // false becomes an empty string
var_dump((string) 2.50); // the trailing zero is not kept
var_dump((array) 'Pune'); // a scalar becomes a one-item array
var_dump((array) null); // null becomes an empty array
var_dump(intval('0x1A', 16)); // hexadecimal
var_dump(intval('101', 2)); // binaryint(1000)
int(9)
int(10)
string(0) ""
string(3) "2.5"
array(1) {
[0]=>
string(4) "Pune"
}
array(0) {
}
int(26)
int(5)settype() converts a variable in place instead of returning a new value. It returns true on success. You will see it in older code; a cast with an assignment, $pin = (int) $pin;, is usually clearer.
<?php
declare(strict_types=1);
$pin = '124001';
settype($pin, 'int');
var_dump($pin);
echo gettype(1.5), ' / ', get_debug_type(1.5), "\n";int(124001) double / float
strict_types: no juggling for function arguments
By default PHP runs in coercive mode: if a function wants an int and you pass '72', PHP converts it. With declare(strict_types=1); as the first statement of a file, calls made from that file must pass the exact type, or PHP throws a TypeError. The one allowed exception is passing an int where a float is expected, because no information is lost.
<?php
declare(strict_types=1);
function seatsLeft(int $total, int $booked): int
{
return $total - $booked;
}
function fare(float $base): float
{
return $base * 1.05;
}
var_dump(fare(400)); // int to float is still allowed
try {
var_dump(seatsLeft('72', '70'));
} catch (TypeError $e) {
echo $e->getMessage(), "\n";
}
$booked = (int) '70'; // convert on purpose, then call
var_dump(seatsLeft(72, $booked));float(420) seatsLeft(): Argument #1 ($total) must be of type int, string given, called in ... on line 17 int(2)
Without the declare line, seatsLeft('72', '70') would quietly return 2. Strict mode makes you convert at one clear place, usually where input enters your program, so the rest of your code can trust its types. Remember that strict_types does not change operators: '5' + 1 still juggles.
Advanced: validate, then convert
A cast never fails. (int) '3 kg' is 3, and (int) '' is 0, with no warning at all. For user input, check the value first. filter_var() with FILTER_VALIDATE_INT validates and converts in one step:
<?php
declare(strict_types=1);
function toQuantity(string $raw): ?int
{
$qty = filter_var($raw, FILTER_VALIDATE_INT, [
'options' => ['min_range' => 1, 'max_range' => 10],
]);
return $qty === false ? null : $qty;
}
foreach (['3', ' 4 ', '12', '2.5', '3 kg', '1e1', ''] as $raw) {
$qty = toQuantity($raw);
$cast = (int) $raw;
printf("%-7s cast: %-3d checked: %s\n", "'$raw'", $cast, $qty === null ? 'rejected' : $qty);
}'3' cast: 3 checked: 3 ' 4 ' cast: 4 checked: 4 '12' cast: 12 checked: rejected '2.5' cast: 2 checked: rejected '3 kg' cast: 3 checked: rejected '1e1' cast: 10 checked: rejected '' cast: 0 checked: rejected
The cast accepted every value, including nonsense. The validated version accepts only whole numbers from 1 to 10. You will learn all the options of filter_var() in Filters & filter_var().
Common mistakes
- Comparing secrets with
==.'0e123' == '0e456'istruebecause both look like the number zero. Use===orhash_equals(). - Forgetting the strict flag in
in_array(). Withouttrueas the third argument,'1e1'is found in['10']. - Casting user input without checking it.
(int) 'abc'is0, which may be a valid id. Validate first. - Expecting
(int)to round. It cuts off the fraction. Useround()first. - Thinking
strict_typesaffects operators. It only affects function and method calls (and return values) in that file.
Key points
- Type juggling is PHP's automatic conversion in arithmetic, joining, conditions,
==and function calls. ===compares value and type with no conversion; use it by default, plusin_array(..., true)andmatch.- Since PHP 8, comparing a number with a non-numeric string compares them as strings, so
0 == 'a'isfalse. - Casts,
intval()andsettype()convert on purpose, but never complain about bad input. declare(strict_types=1);makes function calls in that file throw aTypeErrorfor the wrong scalar type.- Validate input (for example with
filter_var()) before you convert it.
Exercise
Write a file with declare(strict_types=1); and a function discountedPrice(int $price, int $percent): int that returns the price after the discount, rounded down with intdiv(). A form sends $price = '1299' and $percent = '10%'. Convert both values safely: remove a trailing % with rtrim(), then validate each with FILTER_VALIDATE_INT (percent must be 0 to 90). Print the new price, or "Invalid input" if either value fails.
Show solution
<?php
declare(strict_types=1);
function discountedPrice(int $price, int $percent): int
{
return intdiv($price * (100 - $percent), 100);
}
$price = '1299';
$percent = '10%';
$priceInt = filter_var($price, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
$percentInt = filter_var(rtrim($percent, '%'), FILTER_VALIDATE_INT, [
'options' => ['min_range' => 0, 'max_range' => 90],
]);
if ($priceInt === false || $percentInt === false) {
echo "Invalid input\n";
} else {
echo 'New price: ₹' . discountedPrice($priceInt, $percentInt) . "\n";
}New price: ₹1169
Because the file uses strict_types, passing the raw strings would throw a TypeError. Validating first turns them into real ints and rejects values like 'abc' or '95%'.
Quick Quiz
1. What is the result of 0 == 'a' in PHP 8?
Since PHP 8, a number compared with a non-numeric string is compared as a string.
2. Which call finds nothing in ['10', '20']?
With the third argument true, in_array() compares strictly, so the string '1e1' does not equal '10'.
3. What does (int) 9.99 give?
A cast to int cuts off the fraction without rounding and without any warning.
4. With declare(strict_types=1);, which call to function f(float $x) is allowed?
Strict mode still allows an int where a float is expected, because no information is lost; strings are rejected.
Learn PHP with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
