Apparently, three examples of PHP weirdness weren't enough.
In the original post, we explored how PHP considers "0" to be false, how array keys can silently overwrite one another, and how an innocent-looking foreach loop can unexpectedly modify your data.
Those were fun.
But PHP has been around for three decades, and you don't accumulate that much history without developing a few interesting personality traits.
So here are four more examples of PHP doing exactly what it was designed to do, even when that seems deeply unreasonable.
4. Filtering an Array Can Turn It Into a JSON Object
Let's say you're building an API.
You have an array of fruit. You want to remove one item and return the rest as JSON.
Simple enough.
$items = ['apple', 'banana', 'cherry'];
$filtered = array_filter(
$items,
fn($item) => $item !== 'banana'
);
echo json_encode($filtered);
What do you expect?
Probably this:
["apple","cherry"]
What you actually get:
{"0":"apple","2":"cherry"}
Wait. What?
You started with an array. You filtered an array. You encoded an array.
And somehow, you got a JSON object.
Here's why.
PHP's array_filter() preserves the original keys.
After filtering, the array looks like this:
[
0 => 'apple',
2 => 'cherry'
]
When json_encode() encounters a PHP array whose keys aren't consecutive integers starting at zero, it serializes it as a JSON object rather than a JSON array.
Your perfectly reasonable filtering operation has silently changed the shape of your API response.
Imagine the delight of the frontend developer expecting an array.
The fix:
echo json_encode(array_values($filtered));
Which gives you:
["apple","cherry"]
The lesson: If you're filtering arrays for JSON responses, remember that array_filter() preserves keys. Use array_values() when you need a proper JSON array.
5. A Variable Can Exist and Still Not Be Set
Let's talk about isset().
The name seems reasonably self-explanatory.
Surely it tells you whether something exists?
Well, not exactly.
$config = [
'timeout' => null
];
var_dump(isset($config['timeout']));
Result:
bool(false)
But the key is right there!
You can see it. I can see it. PHP can presumably see it.
And yet isset() returns false.
That's because isset() doesn't actually mean "does this variable or key exist?"
It means something closer to "does this exist AND contain a value other than null?"
So we have two functions:
isset($config['timeout']); // false
array_key_exists('timeout', $config); // true
Same array. Same key. Different answers.
And there's a practical consequence.
Suppose null means "disable the timeout," while a missing key means "use the default timeout."
You might write:
$timeout = $config['timeout'] ?? 30;
But the null coalescing operator also falls back when the value is null.
So $timeout becomes 30, even though the configuration explicitly contained null.
If those two states matter, you need to distinguish them:
$timeout = array_key_exists('timeout', $config)
? $config['timeout']
: 30;
Now an explicitly configured null stays null.
The lesson: In PHP, "missing" and "present but null" are different states, but isset() treats them the same. Sometimes that's convenient. Sometimes it's a bug waiting for the right configuration file.
6. Two Different Strings Can Be Equal Because They're Both Zero
Here's a fun one.
var_dump("0e123" == "0e999");
What would you expect?
They're clearly different strings.
Surely the answer is false.
bool(true)
Of course it is.
The explanation involves PHP's numeric-string handling.
Both strings are valid representations of numbers in scientific notation.
0e123 means zero multiplied by ten to the power of 123.
0e999 means zero multiplied by ten to the power of 999.
Both evaluate numerically to zero.
And because PHP's loose comparison operator (==) recognizes both strings as numeric, it compares their numeric values.
So:
"0e123" == "0e999" // true
"0e123" === "0e999" // false
This particular behavior has a wonderful nickname: magic hashes.
Certain hash outputs happen to consist entirely of a zero, followed by e, followed by digits.
When two such hashes are compared using loose equality, PHP can interpret them as equal numeric values even though their string contents differ.
Historically, this has contributed to security vulnerabilities in applications that compared hash strings incorrectly.
And no, switching from PHP 7 to PHP 8 doesn't eliminate this particular issue. Both operands here are valid numeric strings, so the loose comparison still considers them equal.
The fix is straightforward:
"0e123" === "0e999" // false
For security-sensitive hash comparisons, use the appropriate cryptographic comparison functions, such as hash_equals() when comparing authentication-related strings, or password_verify() for password hashes.
The lesson: Loose equality isn't merely a convenience feature. When values originate from external input, it can become a security problem.
7. switch and match Disagree About Reality
PHP 8 introduced match, a more modern alternative to switch.
And naturally, they don't quite agree on what constitutes a match.
Consider:
$value = "10";
switch ($value) {
case 10:
echo "Integer ten";
break;
case "10":
echo "String ten";
break;
}
Output:
Integer ten
The value is a string.
The first case is an integer.
But PHP's switch uses loose comparisons, so "10" matches 10.
The first matching case wins.
Now let's rewrite that using match:
$value = "10";
echo match ($value) {
10 => "Integer ten",
"10" => "String ten",
};
Output:
String ten
Same input. Same alternatives. Different result.
Why?
Because match uses strict comparisons (===), while switch uses loose comparisons (==).
And match has some other differences, including no fall-through and a requirement to handle unmatched values or provide a default.
To be fair, match is arguably the better-designed construct.
But imagine converting an old switch statement to match during a refactoring session, assuming that you're just modernizing the syntax.
You could change application behavior without changing any of the values being compared.
The lesson: When replacing switch with match, you're not merely changing syntax. You're changing comparison semantics.
PHP Is Consistent. That's Not Always Comforting.
Here's the interesting thing about all seven examples we've now explored across these two articles.
None of them are random.
PHP has rules governing truthiness, type coercion, array keys, references, JSON serialization, and comparisons.
The interpreter follows those rules consistently.
The problem is that those rules don't always line up with what developers intuitively expect.
And sometimes two individually reasonable rules interact to produce something spectacularly unreasonable.
That's what makes PHP such a fascinating language to revisit.
It has evolved a lot since the 1990s. Modern PHP is faster, more expressive, and substantially better engineered than the language many developers remember.
But it still has a few surprises hiding behind perfectly ordinary syntax.
And after thirty years, I suspect it's earned the right to be a little eccentric.
Top comments (0)