PHP Type Juggling
What Is Type Juggling? In simple terms: Type juggling is PHP automatically converting a value from one type to another when an operation needs a different type. You do not write a cast — PHP decides o
What Is Type Juggling?
In simple terms: Type juggling is PHP automatically converting a value from one type to another when an operation needs a different type. You do not write a cast — PHP decides on its own.
Technically: PHP does not require explicit type declarations for variables. When an operator, a function, or a language construct expects a specific type, PHP coerces (converts) the operand to that type according to fixed rules. The conversion produces a temporary value; the variable itself keeps its original type.
Why PHP Does This
PHP was designed for the web, where almost all input arrives as text. Automatic conversion lets simple code work without ceremony:
<?php
$quantity = "3"; // from a form, always a string
$price = 250;
echo $quantity * $price;
Output
750
The string "3" is converted to the number 3 for multiplication. This convenience is the reason type juggling exists. The cost is that the rules sometimes produce surprising results, especially in comparisons. Knowing where juggling happens — and how PHP 8 changed it — prevents those surprises.
Juggling vs Casting
| Type Juggling | Type Casting | |
|---|---|---|
| Who decides | PHP, automatically | You, explicitly |
| Syntax | None — happens inside operations | (int) $x, (string) $x, … |
| Visibility in code | Hidden | Obvious |
| Covered in | This lesson | PHP Casting |
Where Type Juggling Happens
| Context | What PHP Converts To | Example |
|---|---|---|
Arithmetic operators (+ - * / % **) | Numbers | "5" + 3 → 8 |
String operator (.) | Strings | 5 . 3 → "53" |
Conditions (if, while, !, &&, ||, ?:) | Booleans | if ("0") → false |
Loose comparison (==, !=, <, >, <=>) | A common type, by comparison rules | "10" == "1e1" → true |
| Array keys | Integer or string | $a["8"] uses key 8 |
| Function arguments and returns (non-strict mode) | The declared scalar type | "21" passed to int $n → 21 |
The following sections cover each context.
Numeric Context: Arithmetic
When a string is used with an arithmetic operator, PHP converts it to a number.
Example
<?php
var_dump("5" + "3");
var_dump("2.5" * 2);
var_dump("10" / "4");
var_dump(" 42" + 0);
var_dump("1e3" + 1);
var_dump(true + true);
var_dump(null + 5);
Output
int(8)
float(5)
float(2.5)
int(42)
float(1001)
int(2)
int(5)
How the Result Type Is Chosen
- If the string looks like an integer (
"5"," 42"), it becomes anint. - If it contains a decimal point or exponent (
"2.5","1e3"), it becomes afloat. truebecomes1,falseandnullbecome0.- Division follows its usual rule: an exact integer result stays
int, otherwisefloat.
Strings That Are Not Fully Numeric
PHP 8 classifies strings into three groups, and arithmetic treats each differently:
| String | Category | Arithmetic Behavior (PHP 8) |
|---|---|---|
"42", " 42", "42 ", "4.2e1" | Numeric (leading and trailing whitespace allowed) | Converted silently |
"42 apples" | Leading-numeric | Uses 42, raises Warning: A non-numeric value encountered |
"apples", "" | Non-numeric | Throws a TypeError |
<?php
var_dump("42 apples" + 1);
Output
PHP Warning: A non-numeric value encountered in /path/script.php on line 2
int(43)
<?php
try {
var_dump("apples" + 1);
} catch (TypeError $e) {
echo "TypeError: " . $e->getMessage() . "\n";
}
Output
TypeError: Unsupported operand types: string + int
Version note: In PHP 7,
"apples" + 1produced only a warning and the result1. PHP 8 made these rules stricter ("Saner numeric strings"), so calculations with clearly invalid data now fail loudly instead of producing wrong numbers.
String Context: Concatenation
The . operator converts both operands to strings:
<?php
var_dump(5 . 3);
var_dump("Total: " . 99.50);
var_dump("Active: " . true);
var_dump("Deleted: " . false);
Output
string(2) "53"
string(11) "Total: 99.5"
string(9) "Active: 1"
string(9) "Deleted: "
99.50 loses its trailing zero, true becomes "1", and false becomes an empty string. These conversions are why echo is unreliable for inspecting values; use var_dump() when debugging.
Boolean Context: Conditions
Any value used as a condition is converted to bool. The falsy values are false, 0, 0.0, "", "0", [], and null; everything else is truthy.
<?php
$values = [0, "0", "", "0.0", " ", [], [0], null, "false"];
foreach ($values as $value) {
echo str_pad(json_encode($value), 8) . ($value ? "truthy" : "falsy") . "\n";
}
Output
0 falsy
"0" falsy
"" falsy
"0.0" truthy
" " truthy
[] falsy
[0] truthy
null falsy
"false" truthy
json_encode() is used here only to show each value with its quotes. The details of boolean conversion are covered in PHP Booleans.
Comparison Context: Loose Comparison (==)
This is the area where type juggling causes the most bugs.
How == Works
== compares values after converting them to a common type. === compares values and types, with no conversion.
PHP 8 Comparison Rules
| Operands | What PHP Does |
|---|---|
| Both numeric strings, or number with numeric string | Compare as numbers |
| Number with non-numeric string | Convert the number to a string and compare as strings (PHP 8.0+) |
| Two non-numeric strings | Compare as strings |
null with a string | Convert null to "" and compare as strings |
bool or null with anything else | Convert both sides to bool |
| Arrays | Equal if they have the same key/value pairs (values compared loosely) |
Example
<?php
$tests = [
['42', '42.0'],
[42, ' 42'],
[42, '42abc'],
[0, 'abc'],
[0, ''],
['1', '01'],
['10', '1e1'],
[100, '1e2'],
[null, false],
[null, 0],
[null, ''],
[null, '0'],
['0', false],
[[], false],
[true, 'false'],
];
foreach ($tests as [$a, $b]) {
printf("%-8s == %-8s %s\n", var_export($a, true), var_export($b, true), var_export($a == $b, true));
}
Output
'42' == '42.0' true
42 == ' 42' true
42 == '42abc' false
0 == 'abc' false
0 == '' false
'1' == '01' true
'10' == '1e1' true
100 == '1e2' true
NULL == false true
NULL == 0 true
NULL == '' true
NULL == '0' false
'0' == false true
array (
) == false true
true == 'false' true
Understanding the Surprising Results
'1' == '01','10' == '1e1'— both strings look numeric, so PHP compares them as numbers. Two different strings are "equal". This matters for codes, tokens, version strings, and hashes that happen to contain only digits.42 == '42abc'isfalsein PHP 8 —'42abc'is not numeric, so42is converted to"42"and the strings differ. In PHP 7, this wastrue.0 == 'abc'isfalsein PHP 8 — in PHP 7 it wastrue, which caused real security bugs (for example, a check comparing against0passed for any string).null == 0istrue, butnull == '0'isfalse— with an integer, both sides become booleans (false == false); with a string,nullbecomes"", and"" == "0"is false as strings.true == 'false'— any non-empty string is truthy.- The empty array is printed by
var_export()across two lines (array (and)), which is why that row looks different.
The Fix: Use ===
<?php
var_dump('1' === '01');
var_dump(null === 0);
var_dump('0' === false);
Output
bool(false)
bool(false)
bool(false)
Strict comparison never converts types, so every result above is what most people expect. Use === and !== by default, and use == only when you deliberately want type conversion.
Comparing Secrets
For comparing passwords, tokens, or signatures, === is still not enough — it can leak timing information. Use hash_equals() for tokens and password_verify() for passwords (see Password Hashing).
Hidden Loose Comparisons in Functions
Several commonly used constructs compare loosely by default.
in_array() and array_search()
<?php
var_dump(in_array("1e1", ["10"]));
var_dump(in_array(null, [0]));
var_dump(in_array("abc", [0]));
var_dump(in_array("1e1", ["10"], true));
var_dump(in_array(null, [0], true));
Output
bool(true)
bool(true)
bool(false)
bool(false)
bool(false)
Without the third argument, in_array() uses ==. Passing true enables strict comparison. The same applies to array_search() and array_keys($array, $value).
Note:
in_array("abc", [0])wastruein PHP 7 and isfalsein PHP 8 because of the comparison change described above.
switch
<?php
$status = "0";
switch ($status) {
case false:
echo "Matched false\n";
break;
case "0":
echo "Matched \"0\"\n";
break;
}
echo match ($status) {
false => "Matched false",
"0" => "Matched \"0\"",
};
Output
Matched false
Matched "0"
switch compares with ==, so "0" == false matches the first case. match (PHP 8.0+) compares with ===, so it finds the intended arm. Prefer match when strict comparison matters; see PHP Match.
Sorting Mixed Types
Functions like sort() compare values loosely by default. Sorting arrays that mix numbers and strings can produce unexpected orders. Keep arrays to a single type, or pass sorting flags such as SORT_STRING or SORT_NUMERIC.
Array Key Juggling
Array keys can only be integers or strings. PHP converts other key types automatically:
<?php
$data = [];
$data["8"] = "string eight";
$data[8] = "integer eight";
$data["08"] = "string zero-eight";
$data[2.0] = "float two";
$data["1.5"] = "string one-point-five";
var_dump($data);
Output
array(4) {
[8]=>
string(13) "integer eight"
["08"]=>
string(17) "string zero-eight"
[2]=>
string(9) "float two"
["1.5"]=>
string(21) "string one-point-five"
}
Key Conversion Rules
| Key Given | Stored As | Reason |
|---|---|---|
"8" | 8 (int) | A string containing a canonical decimal integer becomes an int |
8 | 8 | Same key as "8", so the value is overwritten |
"08" | "08" (string) | Leading zero — not canonical, stays a string |
2.0 | 2 (int) | Floats are truncated to int |
"1.5" | "1.5" (string) | Not an integer string, stays a string |
true / false | 1 / 0 | Booleans become integers |
Why This Matters
$data["8"]and$data[8]are the same element. Data read from a form (string IDs) and from a database (integer IDs) still matches."08"and8are different elements, which can create duplicate entries when IDs are formatted inconsistently.- Using a float with a fractional part as a key (
$data[1.5]) truncates it to1and triggers a deprecation notice since PHP 8.1 ("Implicit conversion from float 1.5 to int loses precision").
Function Arguments: Coercive Mode
By default, PHP runs in coercive typing mode. When a function declares scalar parameter types (int, float, string, bool), PHP converts compatible arguments automatically.
Example
<?php
function double(int $number): int
{
return $number * 2;
}
var_dump(double("21"));
var_dump(double(4.0));
var_dump(double(true));
try {
double("twenty");
} catch (TypeError $e) {
echo "TypeError: " . $e->getMessage() . "\n";
}
Output
int(42)
int(8)
int(2)
TypeError: double(): Argument #1 ($number) must be of type int, string given, called in /path/script.php on line 12
How It Works
"21"is a numeric string, so it is converted to21.4.0has no fractional part, so it becomes4. (A float with a fractional part, such as4.5, would be truncated with a deprecation notice since PHP 8.1.)truebecomes1."twenty"cannot be converted to an integer, so PHP throws aTypeError— type declarations reject clearly incompatible values even in coercive mode.
Return values are coerced the same way: a function declared : int that returns "5" returns int(5).
Strict Mode
Adding declare(strict_types=1); as the first statement of a file disables this coercion for calls made from that file:
<?php
declare(strict_types=1);
function double(int $number): int
{
return $number * 2;
}
try {
double("21");
} catch (TypeError $e) {
echo "TypeError: " . $e->getMessage() . "\n";
}
Output
TypeError: double(): Argument #1 ($number) must be of type int, string given, called in /path/script.php on line 10
In strict mode, only the exact type is accepted — with one exception: an int is allowed where a float is expected, because that conversion never loses information. Strict mode is covered in Strict Types.
Practical Example: A PIN Check Gone Wrong
The following code compares a submitted PIN with a stored PIN.
<?php
$storedPin = "0042"; // stored as a string
$submittedPin = "42"; // user typed 42
if ($submittedPin == $storedPin) {
echo "Loose: access granted\n";
}
if ($submittedPin === $storedPin) {
echo "Strict: access granted\n";
} else {
echo "Strict: access denied\n";
}
Output
Loose: access granted
Strict: access denied
What Went Wrong
- Both values are numeric strings, so
==compares them as numbers:42 == 42. - The user entered a PIN that is not the stored PIN, yet loose comparison accepted it.
===compares the strings exactly and correctly rejects the input.
PINs, phone numbers, ZIP codes, OTPs, and similar codes are identifiers, not quantities. Keep them as strings and compare them strictly (or with hash_equals() for secrets).
Common Mistakes
Using == for Codes and Identifiers
Effect: "0042" == "42" and "1e3" == "1000" are true.
Fix: Use ===.
Relying on PHP 7 Comparison Behavior
Effect: Code that expected 0 == "abc" to be true behaves differently in PHP 8.
Fix: Compare explicitly and strictly; review loose comparisons when upgrading.
in_array() Without Strict Mode
Effect: in_array(null, [0]) is true; in_array("1e1", ["10"]) is true.
Fix: Always pass true as the third argument unless loose matching is intended.
switch With Mixed Types
Effect: "0" matches case false.
Fix: Use match, or normalize the value's type before the switch.
Arithmetic on Unvalidated Input
Effect: TypeError for non-numeric strings, or warnings and partial numbers for strings like "42 apples".
Fix: Validate with filter_var() before calculating.
Inconsistent Array Keys
Effect: "08" and 8 become separate entries.
Fix: Normalize keys (for example, cast IDs to int) before using them.
Best Practices
- Use
===and!==by default. - Pass
trueas the strict flag toin_array(),array_search(), andarray_keys(). - Prefer
matchoverswitchwhen types matter. - Validate and explicitly convert input at the edges of your application; let internal code work with known types.
- Treat codes such as PINs, phone numbers, and ZIP codes as strings.
- Enable
declare(strict_types=1);in new code to make type errors visible early.
Official reference: https://www.php.net/manual/en/language.types.type-juggling.php