HelpWithWebGet Help Now
← Back to Blog
PHP8 min read

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.

ByDino Bartolome
Code editor with colorful syntax highlighting on dark background
Photo by Florian Olivo on Unsplash

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:

ExpressionPHP 7PHP 8
"1" == 1truetrue
"abc" == 0truefalse
null == 0truetrue
null == falsetruetrue
"0" == falsetruetrue
"" == 0truefalse
"10" == "1e1"truetrue (still!)
"100" == "1e2"truetrue (still!)
[] == falsetruetrue

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

Valueisset()empty()is_null()
not declaredfalsetrue(warning)
nullfalsetruetrue
0truetruefalse
"0"truetruefalse
""truetruefalse
falsetruetruefalse
[]truetruefalse
"x"truefalsefalse
1truefalsefalse

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

FunctionMissing file?Already included?
includeWarning, continuesIncludes again
include_onceWarning, continuesSkips
requireFatal error, stopsIncludes again
require_onceFatal error, stopsSkips

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