PHP Gotchas: Equality, Type Coercion, and PHP 8 Changes
PHP's loose equality has caused security bugs for two decades. The == vs === rules, isset vs empty vs is_null, and how PHP 8 quietly fixed some of the worst behaviors.

PHP's type coercion rules are infamous. They caused the famous == security bugs at companies like WordPress, Facebook, and Yahoo — bugs where "0" == "0e123456" would unexpectedly evaluate to true and let attackers bypass authentication.
PHP 8 quietly fixed many of these in 2020, but if you're maintaining older code (or you're reading code at a new job), you need to know both rule sets.
1. Loose equality (==) is the original sin
The classic table everyone needs to memorize:
| Expression | PHP 7 | PHP 8 |
|---|---|---|
"1" == 1 | true | true |
"abc" == 0 | true | false |
null == 0 | true | true |
null == false | true | true |
"0" == false | true | true |
"" == 0 | true | false |
"10" == "1e1" | true | true (still!) |
"100" == "1e2" | true | true (still!) |
[] == false | true | true |
PHP 8 fixed comparisons of strings to numbers when the string is non-numeric. The classic "abc" == 0 → true bug is finally dead in PHP 8.
But `"10" == "1e1"` is still true because both are parsed as numbers. This is the bug that broke password hashing comparisons for years. Always use hash_equals() for cryptographic comparisons.
The fix everywhere else: always use ===. Always.
2. isset() vs empty() vs is_null() — memorize this table
| Value | isset() | empty() | is_null() |
|---|---|---|---|
| not declared | false | true | (warning) |
null | false | true | true |
0 | true | true | false |
"0" | true | true | false |
"" | true | true | false |
false | true | true | false |
[] | true | true | false |
"x" | true | false | false |
1 | true | false | false |
Common bug:
``php
if (empty($_POST['name'])) {
echo "Name is required";
}
``
If name = "0", this incorrectly says it's empty. Use if (!isset($_POST['name']) || $_POST['name'] === '') instead.
3. == on arrays compares element-wise; === requires order
``php
$a = ['a' => 1, 'b' => 2];
$b = ['b' => 2, 'a' => 1];
$a == $b; // true (same keys/values, any order)
$a === $b; // false (different order)
``
This catches devs migrating from JS where object key order is not guaranteed.
4. Single vs double quotes
``php
$name = "Alice";
echo 'Hello $name'; // Hello $name (literal)
echo "Hello $name"; // Hello Alice (interpolated)
echo 'Hello \n'; // Hello \n (literal backslash-n)
echo "Hello \n"; // Hello (newline) (escape)
``
Single quotes are slightly faster but mainly: use single when you don't need interpolation, use double when you do. Don't use double-quoted strings everywhere "just in case" — it makes intent unclear.
5. strpos() returns 0 when match is at the start
```php $pos = strpos("hello world", "hello"); // Returns 0 — but if (!$pos) treats 0 as falsy!
if ($pos === false) { echo "not found"; } else { echo "found at $pos"; } ```
Always check with === false, not !. This bug has produced thousands of CVEs.
6. include vs require vs _once
| Function | Missing file? | Already included? |
|---|---|---|
include | Warning, continues | Includes again |
include_once | Warning, continues | Skips |
require | Fatal error, stops | Includes again |
require_once | Fatal error, stops | Skips |
Use require_once for class/function definitions. Use include for templates that are OK to skip.
7. global keyword and variable scope
PHP functions don't have access to the outer scope unless you explicitly declare:
```php $x = 1; function f() { echo $x; // Notice: undefined variable }
function g() { global $x; echo $x; // 1 } ```
Avoid global. Pass values as parameters. global is a code-smell flag in modern PHP.
8. array_merge re-indexes numeric keys; + doesn't
```php array_merge([1, 2], [3, 4]); // [1, 2, 3, 4] — re-indexed
[1, 2] + [3, 4]; // [1, 2] — left's keys win; right's are ignored
array_merge(['a' => 1], ['a' => 2]); // ['a' => 2] — right wins
['a' => 1] + ['a' => 2]; // ['a' => 1] — left wins ```
If you want associative merge where right wins, use array_merge. If you want left to win and only fill missing keys, use +.
9. References in foreach
```php $arr = [1, 2, 3]; foreach ($arr as &$val) { $val *= 2; } // $arr is now [2, 4, 6]
foreach ($arr as $val) { // ... } // The last $val is STILL a reference to $arr[2] // $arr[2] is now corrupted on subsequent loops ```
Always unset($val) after a foreach-by-reference loop. This bug is shockingly common.
10. null safe operator and nullish coalescing
PHP 8 added two huge quality-of-life operators:
``php
$name = $user?->profile?->name ?? 'Anonymous';
``
?->short-circuits to null if the left is null??returns the right if the left is null (or unset, no warning)
Compare to old PHP:
``php
$name = isset($user) && isset($user->profile) && isset($user->profile->name)
? $user->profile->name
: 'Anonymous';
``
11. Type juggling in switch statements
``php
switch ($x) {
case 0:
echo "zero or empty string";
break;
case "hello":
echo "hello";
break;
}
``
switch uses == (loose comparison). If $x = "hello", the first case (case 0) might match in PHP 7 because "hello" == 0 is true. PHP 8 fixed this for strings, but it's still a gotcha for older code.
Use match (PHP 8) instead — it uses ===:
``php
$result = match($x) {
0 => "zero",
"hello" => "hello",
default => "other",
};
``
Need help with PHP modernization?
If you're sitting on a PHP 5/7 codebase that needs to migrate to PHP 8, we do that work. PHP 8's stricter type coercion catches dozens of latent bugs — but only if you know what to look for.
Need Help With Your Website?
I fix these problems every day. Send me a message and I'll take a look.
Get Help Now