Equal or identical. How to compare variables?
== or ===? How many equal signs to put up so that it is correct and that nobody in the code review has a problem with it? Why is it so tricky in PHP?
Type Juggling
In PHP, the type of a variable is defined by how it was used*. Depending on what we have assigned to variable, it becomes that type1. What does it mean?
(* From PHP 7 we can define types of method arguments, from 7.4 it is possible to define types of class properties!2)
$foo = "1"; // $foo is string
$foo += 0; // $foo is integer
$foo *= 1.5; // $foo is float
In example above, string "1" has been assigned to the variable, so variable is string type, then integer 0 was added to it so it changes its type to integer. After that, let's multiply the integer variable by float 1.5. The variable is now float type.
This example shows how automatic conversion is performed by operators, in this case adding and multiplying. It is enough that one of the sides of the action is of the float type, then both will be treated as float and the result is also going to be of this type. The case is similar with integer although float is "stronger" and in the case of integer + float both sides will be treated as float.
Unequal equal
Let's get to equality, first look at the php documentation3 :
Equal: $a == $b, TRUE if $a is equal to $b after type juggling.
Sounds trivial and looks natural. Type juggling results in that, there is no need to think too much about what we compare, all is done automatically... ;) but is it?
Time for a quiz, will the following conditions be met?
0 == "eleven"
0 == "wtf-1.3e3"
1 == "\n\r1\t"
10 == "10 - 10"
"-1300" == "-1.3e3"
9223372036854775807 == "9223372036854775811"
"1.00000000000000001" == "1.00000000000000002"
Now attention, drums... in each of above cases it will be true. What just happened here? The, so far, highly intuitive comparison operator has now behaved in a manner rather unnatural to the eye.
How does the comparison operator actually work? To find out, it's best to look at how it was implemented4. However, this is not a simple reading, because the code is very complicated there. In short, the operation of == depends on the type of its arguments. Pairs of argument types are defined. If a given pair is supported (it is known how to compare it), the result of the comparison is returned, but if the given pair of types is not supported, the arguments are cast to another type (Type Juggling) and are only then compared. In most cases this works in a predictable manner, but there are pairs of types that can surprise.
numeric value - string with a number and something extra
When string is compared with integer or float, then the string is cast to a numeric value, but this casting can sometimes be surprising.
10 == "10 - 10"; // true
In this case, the value of string is cast on to the integer, if string starts with a number, then whatever goes after that number is ignored. In this case "10 - 10" will not be the result of subtraction, so not 0 but 10.
numeric value - string with a non-number at the beginning
0 == "Lorem ipsum dolor sit 0"; // true
If string does not start with a number, then along with a numeric value it will always be cast on to 0.
numeric value - string with a big number
9223372036854775807 == "9223372036854775811"; // true
The integer from this comparison is the maximum integer value - PHPINTMAX (on 64-bit platforms), the string on the other hand includes a number slightly higher, just at 4 ;). In this case the integer will be cast to float, so the string eventually also to float. Such is the nature of float.
So never trust floating number results to the last digit, and do not compare floating point numbers directly for equality.
It's a warning from PHP documentation5 so it's better not to compare float values directly, but use dedicated functions:
numeric value - string with a number and white characters
1.0 == "\n\r1\t"; // true
White characters are ignored when casting to a numeric value. If you look at the example above you'll find one "\n\r1\t".
string - string
When comparing two string values it would be natural if it were true when they are the same ;) but what does it mean "the same".
"\n1" == "\r1"; // true
"1.0" == "\t1"; // true
"1e0" == "1"; // true
"-0.5e-2" == "-0.005"; // true
"1.00000000000000001" == "1.00000000000000009"; // true
"10" == "0xA"; // true PHP < 7
// false PHP >= 7
When comparing string type arguments, at the beginning both arguments are cast to a numeric value. If it succeeds, they are compared as numerical values. Interestingly, PHP can do a lot of tricks when searching for a number in string6. Scientific notation or leading whitespace are not a problem, as well as hexadecimal notation up to version 7.0.
object - object
If it was too clear and predictable up to now, it works a little different when comparing objects.
class Example {
public $property = 1;
}
$foo = new Example();
$bar = new Example();
$bar->property = 1.0;
$foo == $bar; // true
$foo === $bar; // false
Considering what I wrote earlier, everything in the example above is correct. The $foo and $bar variables are of the same type (Example class), their property ($property) in $foo is equal to 1 (integer) and in $bar is equal to 1.0 (float). Thus, the result of the == comparison is true. Because the values match, types don't have to (integer vs. float). For === the result is false because the class property types do not match?
$baz = new Example();
$qux = new Example();
$baz == $qux; // true
$baz === $qux; // false
What happened here? Both variables are of the same type and their property is of the same value and of the same type, and still the result of === is false. When comparing objects, identity (===) will return as true only if the same instance of the same class is compared7 . Yes. It is not enough for a variable's type (class) and all its properties to match, it must be same object. Comparison == will compare the object type and compare all its properties (also with ==).
So how should I compare?
In the examples I showed, it is clear that using the == comparison, sometimes may give a result different from what we expect. How to deal with this? Every time you write == (two equal signs), a red light should start flashing. We should always use the identity operator === (three equal signs), which also compares the type of the variable and does not cast anything before that. Well, almost always.
The only time we can use the equality == instead of identity operator === is when we do it intentionally and we have reasons for that!
-
https://www.php.net/manual/en/language.types.type-juggling.php
↩ -
https://www.php.net/manual/en/migration74.new-features.php
↩ -
https://www.php.net/manual/en/language.operators.comparison.php
↩ -
https://github.com/php/php-src/blob/PHP-7.4.2/Zend/zend_operators.c#L2022
↩ -
https://www.php.net/manual/en/language.types.float.php
↩ -
https://github.com/php/php-src/blob/PHP-7.4.2/Zend/zend_operators.c#L2908
↩ -
https://www.php.net/manual/en/language.oop5.object-comparison.php
↩