Equal or identical. How to compare variables?
Tags: php, programming, webdev
Published: 2022-07-27 05:50:13


<span class="token operator">==</span> or <span class="token operator">===</span>? 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 type**[^1]. What does it mean?

<small>(\* From PHP 7 we can define types of method arguments, from 7.4 it is possible to define types of class properties![^2])</small>

```php
$foo = "1";  // $foo is string
$foo += 0;   // $foo is integer
$foo *= 1.5; // $foo is float
```

In example above, string <span class="token string">"1"</span> has been assigned to the variable, so variable is <span class="token keyword">string</span> type, then <span class="token keyword">integer</span> <span class="token number">0</span> was added to it so it changes its type to <span class="token keyword">integer</span>. After that, let's multiply the <span class="token keyword">integer</span> variable by <span class="token keyword">float</span> <span class="token number">1.5</span>. The variable is now <span class="token keyword">float</span> 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 <span class="token keyword">float</span> type, then both will be treated as <span class="token keyword">float</span> and the result is also going to be of this type. The case is similar with <span class="token keyword">integer</span> although <span class="token keyword">float</span> is "stronger" and in the case of <span class="token keyword">integer</span> + <span class="token keyword">float</span> both sides will be treated as <span class="token keyword">float</span>.

## Unequal equal

Let's get to equality, first look at the php documentation[^3] :

<blockquote>Equal: $a == $b, TRUE if $a is equal to $b after type juggling.</blockquote>

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 automaticall**y... ;) but is it?

Time for a quiz, will the following conditions be met?

```php
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 <span class="token boolean">true</span>**. 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 implemented[^4]. However, this is not a simple reading, because the code is very complicated there. In short, the operation of <span class="token operator">==</span> **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 <span class="token keyword">string</span> is compared with <span class="token keyword">integer</span> or <span class="token keyword">float</span>, then the <span class="token keyword">string</span> is cast to a numeric value, but this casting can sometimes be surprising.

```php
10 == "10 - 10"; // true
```

In this case, the value of <span class="token keyword">string</span> is cast on to the <span class="token keyword">integer</span>, if <span class="token keyword">string</span> starts with a number, then **whatever goes after that number is ignored**. In this case <span class="token string">"10 - 10"</span> will not be the result of subtraction, so not <span class="token number">0</span> but <span class="token number">10</span>.

### numeric value - string with a non-number at the beginning

```php
0 == "Lorem ipsum dolor sit 0"; // true
```

If <span class="token keyword">string</span> does not start with a number, then along with a numeric value **it will always be cast on to <span class="token number">0</span>**.

### numeric value - string with a big number

```php
9223372036854775807 == "9223372036854775811"; // true
```

The <span class="token keyword">integer</span> from this comparison is the maximum <span class="token keyword">integer</span> value - PHP_INT_MAX (on 64-bit platforms), the <span class="token keyword">string</span> on the other hand includes a number slightly higher, just at 4 ;). In this case the <span class="token keyword">integer</span> will be cast to <span class="token keyword">float</span>, so the <span class="token keyword">string</span> eventually also to <span class="token keyword">float</span>. Such is the nature of <span class="token keyword">float</span>.

<blockquote>
So never trust floating number results to the last digit, 
and do not compare floating point numbers directly for equality.
</blockquote>

It's a warning from PHP documentation[^5] so **it's better not to compare <span class="token keyword">float</span> values directly**, but use dedicated functions:

### numeric value - string with a number and white characters

```php
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 <span class="token comment">"\n\r</span><span class="token string">1</span><span class="token comment">\t"</span>**.

### string - string

When comparing two <span class="token keyword">string</span> values it would be natural if it were **<span class="token boolean">true</span> when they are the same** ;) but what does it mean "the same".

```php
"\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 <span class="token keyword">string</span> 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 string**[^6]. 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.

```php
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 <span class="token variable">$foo</span> and <span class="token variable">$bar</span> variables are of the same type (Example class), their property (<span class="token variable">$property</span>) in <span class="token variable">$foo</span> is equal to 1 (<span class="token keyword">integer</span>) and in <span class="token variable">\$bar</span> is equal to 1.0 (<span class="token keyword">float</span>). Thus, the result of the <span class="token operator">==</span> comparison is <span class="token boolean">true</span>. Because the values match, types don't have to (<span class="token keyword">integer</span> vs. <span class="token keyword">float</span>). **For <span class="token operator">===</span> the result is <span class="token boolean">false</span>** because the class property types do not match?

```php
$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 <span class="token operator">===</span> is <span class="token boolean">false</span>**.
When comparing objects, identity (<span class="token operator">===</span>) will return as <span class="token boolean">true</span> only if the same instance of the same class is compared[^7] . Yes. It is not enough for a variable's type (class) and all its properties to match, it must be same object. Comparison <span class="token operator">==</span> will compare the object type and compare all its properties (also with <span class="token operator">==</span>).

## So how should I compare?

In the examples I showed, it is clear that using the <span class="token operator">==</span> comparison, sometimes may give a result different from what we expect. How to deal with this? Every time you write <span class="token operator">==</span> (two equal signs), a red light should start flashing. We should always use the identity operator <span class="token operator">===</span> (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 <span class="token operator">==</span> instead of identity operator <span class="token operator">===</span> is when we do it intentionally and we have reasons for that**!

[^1]: <a href="https://www.php.net/manual/en/language.types.type-juggling.php" target="_blank">https://www.php.net/manual/en/language.types.type-juggling.php</a>
[^2]: <a href="https://www.php.net/manual/en/migration74.new-features.php" target="_blank">https://www.php.net/manual/en/migration74.new-features.php</a>
[^3]: <a href="https://www.php.net/manual/en/language.operators.comparison.php" target="_blank">https://www.php.net/manual/en/language.operators.comparison.php</a>
[^4]: <a href="https://github.com/php/php-src/blob/PHP-7.4.2/Zend/zend_operators.c#L2022" target="_blank">https://github.com/php/php-src/blob/PHP-7.4.2/Zend/zend_operators.c#L2022</a>
[^5]: <a href="https://www.php.net/manual/en/language.types.float.php" target="_blank">https://www.php.net/manual/en/language.types.float.php</a>
[^6]: <a href="https://github.com/php/php-src/blob/PHP-7.4.2/Zend/zend_operators.c#L2908" target="_blank">https://github.com/php/php-src/blob/PHP-7.4.2/Zend/zend_operators.c#L2908</a>
[^7]: <a href="https://www.php.net/manual/en/language.oop5.object-comparison.php" target="_blank">https://www.php.net/manual/en/language.oop5.object-comparison.php</a>