Type Juggling and Comparisons
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against PHP 8.5, checked on 2026-10-05, with every result shown below produced by PHP 8.5.11; the before/after table also shows PHP 7.4’s results. PHP converts values between types automatically in many places — type juggling — and lets you convert them explicitly with casts. Knowing exactly when and how this happens is what separates a working comparison from a security bug. The types themselves are described in Types and the Type System.
Type juggling contexts
PHP does not require you to declare the type of a variable; instead, an operation determines the type its operands are converted to. The manual distinguishes these contexts:
| Context | What happens |
|---|---|
Numeric |
Arithmetic operators ( |
String |
|
Logical |
|
Integral and string |
Bitwise operators: both operands are converted to |
Comparative |
|
Function |
Passing an argument to a typed parameter or returning a value: governed by |
<?php
declare(strict_types=1);
$copies = '3';
var_dump($copies + 2); // int(5) numeric context
var_dump($copies . ' left'); // string(6) "3 left" string context
var_dump(!$copies); // bool(false) logical context
var_dump('12' | 1); // int(13) integral context
var_dump('a' | 'b'); // string(1) "c" two strings: bytewise
Note that strict_types does not change these operator conversions: it applies to parameters, returns and typed
properties only.
Source: Type Juggling
Explicit casts
A cast is written as the target type in parentheses before the value. PHP 8.5 accepts only the canonical names below;
the aliases (integer), (boolean), (double) and (binary) are deprecated since 8.5, and (real) and
(unset) were removed in 8.0.
| Cast | Deprecated alias | Notes |
|---|---|---|
|
|
floats are truncated towards zero; see Float to string and float to int below for |
|
|
|
|
|
|
|
|
the falsy rules |
|
a scalar becomes a one-element array; |
|
|
an array becomes a |
<?php
declare(strict_types=1);
$inputs = [
"'42'" => '42', "'42abc'" => '42abc', "'abc'" => 'abc', "' 42'" => ' 42', "'4.9'" => '4.9',
"'1e3'" => '1e3', "'0x1A'" => '0x1A', "'012'" => '012', 'null' => null, 'true' => true,
'4.9' => 4.9, '-4.9' => -4.9, '[]' => [], '[1]' => [1], "''" => '',
];
foreach ($inputs as $label => $v) {
printf("%-8s int:%-5s float:%-7s bool:%s\n",
$label, var_export((int) $v, true), var_export((float) $v, true), var_export((bool) $v, true));
}
'42' int:42 float:42.0 bool:true
'42abc' int:42 float:42.0 bool:true
'abc' int:0 float:0.0 bool:true
' 42' int:42 float:42.0 bool:true
'4.9' int:4 float:4.9 bool:true
'1e3' int:1000 float:1000.0 bool:true
'0x1A' int:0 float:0.0 bool:true
'012' int:12 float:12.0 bool:true
null int:0 float:0.0 bool:false
true int:1 float:1.0 bool:true
4.9 int:4 float:4.9 bool:true
-4.9 int:-4 float:-4.9 bool:true
[] int:0 float:0.0 bool:false
[1] int:1 float:1.0 bool:true
'' int:0 float:0.0 bool:false
Three things to notice: casts never fail (they convert as much of a leading-numeric string as they can, and silently
produce 0 for a non-numeric one), hexadecimal and octal strings are not parsed by casts ('0x1A' is 0, '012'
is 12), and (int) truncates instead of rounding. Casting is therefore the wrong tool for validating input; use
filter_var($v, FILTER_VALIDATE_INT) or is_numeric() first (see
Forms, Validation and Output Escaping).
Source: Type Casting and PHP 8.5 deprecated features (non-canonical cast names)
Conversion functions
<?php
declare(strict_types=1);
// intval() can parse other bases; base 0 detects the prefix (0x, 0, 0b).
var_dump(intval('0x1A', 16), intval('0x1A', 0), intval('012', 0), intval('101', 2)); // 26 26 10 5
var_dump(floatval('1.5e3 copies')); // float(1500)
var_dump(boolval('0'), strval(3.0)); // bool(false) string(1) "3"
// settype() changes the variable in place and returns true on success.
$year = '1979';
settype($year, 'int');
var_dump($year); // int(1979)
// gettype() uses historical names; get_debug_type() uses declaration names.
var_dump(gettype(1.5), get_debug_type(1.5)); // string(6) "double" string(5) "float"
The is_* functions test without converting: is_int, is_float, is_string, is_bool, is_array,
is_object, is_null, is_callable, is_iterable, is_countable, is_resource, is_scalar and the looser
is_numeric (true for numeric strings).
Source: intval, settype and get_debug_type
Float to string and float to int
Converting a float to a string uses the shortest representation that round-trips (0.1 becomes "0.1", 1.0
becomes "1"), independent of the locale since PHP 8.0. Converting a float to int truncates. NAN and INF become 0, and a float outside the int range wraps
around to a meaningless value; since PHP 8.5 such casts emit a warning ("The float INF is not representable as an
int, cast occurred"). Check is_finite() and the range before casting. Implicitly converting a float with a fractional part to int (for example as an
array key or a coercive int parameter) is deprecated since 8.1: "Implicit conversion from float 4.9 to int loses
precision".
Source: From floating point numbers and Converting to string
Loose and strict comparison
PHP has two equality operators:
-
===(identical) is true only if both operands have the same type and the same value. No conversion happens. -
==(equal) first juggles the operands to a common type, then compares.
!== and != (or <>) are their negations. Use === by default. Reach for == only when you genuinely want
juggling, for example comparing two objects' properties.
The PHP 8 change: saner string-to-number comparisons
Before PHP 8.0, comparing a number with a string always converted the string to a number, so any non-numeric string
compared equal to 0. Since PHP 8.0, a number and a string are compared as numbers only if the string is numeric;
otherwise the number is converted to a string and the two are compared as strings. The RFC that introduced this is
"Saner string to number comparisons".
| Expression | PHP 7.4 | PHP 8.5 | Why |
|---|---|---|---|
|
|
|
|
|
|
|
the empty string is not numeric |
|
|
|
same rule, operands reversed |
|
|
|
leading-numeric is not numeric |
|
|
|
leading-numeric |
|
|
|
trailing whitespace became allowed in numeric strings |
|
|
|
|
|
|
|
numeric string (leading whitespace) |
|
|
|
two numeric strings are compared as numbers |
|
|
|
numeric strings, |
|
|
|
numeric string |
The last three rows show that the rule still bites with two strings: if both are numeric, == compares them
numerically. Two different password hashes or tokens that both look like 0e… numbers ("magic hashes") compare
equal with ==. Always compare secrets with hash_equals(), and strings with ===.
<?php
declare(strict_types=1);
var_dump(0 == 'foo'); // bool(false) since 8.0 (true in PHP 7)
var_dump('1e3' == '1000'); // bool(true) -- both numeric strings!
var_dump('0e1234' == '0e5678'); // bool(true) -- both are "0" in scientific notation
var_dump('0e1234' === '0e5678'); // bool(false)
var_dump(hash_equals('0e1234', '0e5678')); // bool(false) -- and timing-safe
How == compares each pair of types
| Operands | Comparison |
|---|---|
|
|
|
both sides converted to |
|
same class and equal properties ( |
|
numeric strings and resources are converted to numbers, then usual math; non-numeric strings compared as strings (8.0 rule) |
|
the array with fewer elements is smaller; if a key from operand 1 is not in operand 2 they are uncomparable; otherwise compared value by value |
|
the object is always greater |
|
the array is always greater |
<?php
declare(strict_types=1);
var_dump(null == false, null == 0, null == '', '0' == false, '' == false, [] == false);
// all bool(true): the bool conversion rule
var_dump('0.0' == false); // bool(false): '0.0' is a non-empty string other than '0', so it is true
var_dump('abc' == 'ABC'); // bool(false): case-sensitive
// Arrays: == ignores order of keys, === does not
var_dump(['a' => 1, 'b' => 2] == ['b' => 2, 'a' => 1]); // bool(true)
var_dump(['a' => 1, 'b' => 2] === ['b' => 2, 'a' => 1]); // bool(false)
Source: Comparison Operators and PHP type comparison tables
The loose comparison table
The manual’s type comparison tables appendix gives == for every pair of common values. An abridged PHP 8 version
(T = true, F = false):
== |
true |
false |
1 |
0 |
-1 |
"1" |
"0" |
"" |
null |
[] |
|---|---|---|---|---|---|---|---|---|---|---|
|
T |
F |
T |
F |
T |
T |
F |
F |
F |
F |
|
F |
T |
F |
T |
F |
F |
T |
T |
T |
T |
|
T |
F |
T |
F |
F |
T |
F |
F |
F |
F |
|
F |
T |
F |
T |
F |
F |
T |
F |
T |
F |
|
T |
F |
F |
F |
T |
F |
F |
F |
F |
F |
|
T |
F |
T |
F |
F |
T |
F |
F |
F |
F |
|
F |
T |
F |
T |
F |
F |
T |
F |
F |
F |
|
F |
T |
F |
F |
F |
F |
F |
T |
T |
F |
|
F |
T |
F |
T |
F |
F |
F |
T |
T |
T |
|
F |
T |
F |
F |
F |
F |
F |
F |
T |
T |
In the strict (===) table only the diagonal is true.
Source: PHP type comparison tables
Ordering: <, > and the spaceship operator <=>
<, <=, >, >= use the same juggling rules as ==. The spaceship operator $a <=> $b (7.0) returns -1,
0 or 1 when $a is less than, equal to or greater than $b, which is exactly what sorting callbacks must return:
<?php
declare(strict_types=1);
var_dump(1 <=> 2, 'b' <=> 'a', [1, 2] <=> [1, 3], [1, 2, 3] <=> [1, 2]); // -1 1 -1 1
$books = [
['title' => 'Solaris', 'year' => 1961],
['title' => 'Kindred', 'year' => 1979],
['title' => 'Dawn', 'year' => 1979],
];
// Sort by year descending, then by title ascending: compare arrays element by element.
usort($books, fn (array $a, array $b): int => [$b['year'], $a['title']] <=> [$a['year'], $b['title']]);
echo implode(', ', array_column($books, 'title')), "\n"; // Dawn, Kindred, Solaris
Source: Comparison Operators (spaceship operator)
Comparing objects
== on two objects of the same class compares all properties with ==; === is true only for the same
instance. Enum cases are singletons, so === is the natural comparison. DateTime and DateTimeImmutable compare
by the instant they represent with <, == and >, even across the two classes.
<?php
declare(strict_types=1);
final class Isbn { public function __construct(public string $value) {} }
$a = new Isbn('978-0-00-000001-1');
$b = new Isbn('978-0-00-000001-1');
var_dump($a == $b, $a === $b, $a === $a); // bool(true) bool(false) bool(true)
var_dump(new DateTimeImmutable('2026-01-01') < new DateTime('2026-06-01')); // bool(true)
Source: Comparing Objects
Pitfalls in searching, sorting and switching
Several functions and constructs use == by default:
-
in_array()andarray_search()compare loosely unless the third argument$strictistrue. -
array_keys($a, $value)andarray_unique()(which compares the string representation by default) also juggle. -
switchcompares with==;matchcompares with===, which is one of the reasons to prefermatch(see Control Structures). -
sort()without flags compares values with the standard rules, so mixing numbers and strings gives orders that are hard to predict; passSORT_STRING,SORT_NUMERICorSORT_NATURALto state the intent. -
Array keys are juggled too:
'1',1,1.7(with a deprecation) andtrueall become the key1(see Arrays).
<?php
declare(strict_types=1);
var_dump(in_array('1e1', ['10'])); // bool(true) -- '1e1' == '10'
var_dump(in_array('1e1', ['10'], true)); // bool(false)
var_dump(array_search('0', [false, 0])); // int(0) -- '0' == false
$shelfCodes = ['10', '9', '2', '1'];
sort($shelfCodes); // numeric strings compared as numbers
echo implode(' ', $shelfCodes), "\n"; // 1 2 9 10
sort($shelfCodes, SORT_STRING);
echo implode(' ', $shelfCodes), "\n"; // 1 10 2 9
$status = '0';
echo match ($status) { // strict comparison: '0' !== 0
0 => "zero (int)\n",
'0' => "zero (string)\n",
};
Where older books and tutorials are outdated
-
String-to-number comparisons. Books written for PHP 5 and 7 (the four consulted for this section among them) teach that
0 == "any string"is true and give examples such as"12 o'clock" == 12evaluating totrue. Since PHP 8.0 both arefalse; the before/after table above lists the changes. -
Arithmetic on non-numeric strings. Before 8.0 it produced a warning and used
0; since 8.0 it throwsTypeError. Leading-numeric strings ("12 books" + 1) still work, with a warning. -
Casts. The
(unset)cast and(real)were removed in 8.0;(integer),(boolean),(double)and(binary)are deprecated since 8.5. -
Float to string. Before 8.0,
(string) 3.5depended on the locale set withsetlocale()("3,5"in a German locale); it is always"3.5"now.
References
-
PHP Manual — Type Juggling (including type casting)
-
intval, settype, gettype, get_debug_type, in_array, sort, hash_equals