Types & comparison
Why == and === are not the same comparison
The mistake
It is tempting to read $a == $b as “are these the same”. So 0 == "foo" looks
false, and "1" == "01" looks false too, because the strings are different.
Both readings are wrong. == almost never compares the two values as they are. It
first coerces them to a common type, following a fixed ladder of rules, and only
then compares. "1" == "01" is true, because both are numeric strings and compare as
numbers. And 0 == "foo" was true for years, until PHP 8 changed one rule.
The machine
Every result and every step runs on the tested reducer, matching the php.net rules.
Drive it
The panel shows the two values, then each step of the juggling, then the result.
- Pick
0on the left and"abc"on the right. On PHP 8 the result is false. Press See what PHP 7 did and it flips to true. That one rule is the change. - Try
"1"and"01". Different strings, but both numeric, so they compare as numbers and the result is true. - Switch to
===. Now1 === "1"is false. Strict comparison checks the type first and never juggles.
The mechanism
== walks a ladder of rules and stops at the first one that matches the pair of
types. The rules that catch most people:
- Two strings compare as numbers if both are numeric strings, otherwise letter by
letter. So
"1" == "01"is true, but"abc" == "abcd"is false. - A bool or a null on either side turns both sides into booleans. So
"abc" == trueis true, and0 == nullis true. Butnull == "0"is false, because null against a string is a string comparison where null becomes"", and"" != "0". - A number against a numeric string compares as numbers, so
100 == "1e2"is true.
The rule that changed in PHP 8 is a number against a non-numeric string. PHP 7 cast
the string to a number, and a non-numeric string became 0, so 0 == "foo" was true.
PHP 8 casts the number to a string instead, so it compares "0" with "foo", which is
false. Numeric-string comparisons like 42 == "42" were not affected.
=== does none of this. It checks that the types are identical first, and only then
compares the values. That is why 1 === "1" is false and 1 === 1.0 is false too.
In your code
0 == 'foo'; // false on PHP 8, true before it
'1' == '01'; // true, both are numeric strings
'1e2' == '100'; // true
null == ''; // true
null == '0'; // false
0 === 'foo'; // false, no juggling
'1' === 1; // false, a string is not an int
// Comparing anything that came from outside? Prefer ===, and pass the
// strict flag to functions that take one:
in_array('1', [1, 2, 3], true); // false, no juggling
switch compares with ==, so it juggles the same way. When the cases are strings
that could look numeric, reach for match, which compares strictly.
The fine print
- The relational operators (
<,>,<=>) juggle too, with their own rules. - Comparing arrays looks at size first, then keys and values. Comparing objects looks at their properties, or a class can define its own comparison.
- What counts as a numeric string has edge cases, like leading whitespace and trailing characters, which PHP 8 also tidied up.
- This page uses a small set of values to keep the rules visible. The real ladder covers resources and more type pairs.
Further reading
- PHP: comparison operators has the full loose-comparison table, every type pair, straight from the manual.
- PHP: type juggling
explains the casting that
==relies on, in each context it happens. - RFC: saner string to number comparisons
is the PHP 8 change itself, with the reasoning behind making
0 == "foo"false.
Spotted a problem, or have a way to make this clearer? Suggest an improvement.