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

Simulator · PHP comparison

Every result and every step runs on the tested reducer, matching the php.net rules.

"1" == "01" is true on PHP 8.

Drive it

The panel shows the two values, then each step of the juggling, then the result.

  • Pick 0 on 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 ===. Now 1 === "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" == true is true, and 0 == null is true. But null == "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

Spotted a problem, or have a way to make this clearer? Suggest an improvement.