gap·map

PHP types & modern OOP

[ middle depth ]

How == and === differ in PHP

=== compares type and value with no conversion. == runs type juggling first, and that is where the classic bugs and interview traps live. Since PHP 8 the number-vs-string rule changed, so version matters:

var_dump('0' == false);    // true  - both are falsy
var_dump('1e3' == '1000'); // true  - two numeric strings, compared as numbers
var_dump(0 == 'foo');      // false in PHP 8, was true in PHP 7
var_dump(0 == '');         // false in PHP 8, was true in PHP 7

The PHP 8 rule: comparing a number to a string only compares them as numbers when the string is numeric. Otherwise PHP turns the number into a string and compares text. 'foo' and '' are not numeric, so 0 becomes '0' and the comparison fails.

Wrong "PHP 8 fixed loose comparison, so == is safe now." Two numeric strings still compare as numbers, which is why '0e123' == '0e456' is true. On tokens, hashes, or anything security-adjacent, use === or hash_equals.

Arrays copy, objects don't

This is the single most common "why did my data change" bug in PHP. Arrays assign by value; objects assign by handle.

$a = [1, 2, 3];
$b = $a;
$b[] = 4;            // $a is still [1, 2, 3]

$x = new ArrayObject([1, 2, 3]);
$y = $x;
$y[] = 4;            // $x now has the 4 too - same object

Passing an array to a function gives that function its own logical copy, so mutations inside stay inside unless the parameter is typed &$arr. Passing an object passes the handle, so the function mutates your object. When you actually want an independent object, use clone (a shallow copy).

Null handling: ??, ?->, and isset

?? returns the right side when the left is null or was never set, and it suppresses the undefined-key warning:

$name = $data['name'] ?? 'guest';       // no warning if 'name' is missing
$city = $user?->address?->city;         // whole chain is null if any link is null
$config['timeout'] ??= 30;              // assign only if null/unset

$a ?? $b behaves like isset($a) ? $a : $b, so it treats null and undefined the same. Do not reach for ?: (the Elvis operator) as a substitute: ?: checks falsiness, so 0, '', and '0' all fall through to the default, and it warns on an undefined key. Use ?? for "missing or null", ?: only when you genuinely want "any falsy value".

[ senior depth ]

What strict_types actually enforces

declare(strict_types=1) is a per-file directive that must be the first statement. It removes scalar coercion at typed function boundaries: pass a '5' to a parameter typed int and you get a TypeError instead of a silent cast to 5. It does nothing beyond that boundary: ==, array-to-string casts, string interpolation, and every other internal conversion behave the same with or without it.

Two subtleties interviewers use to separate levels. First, the mode is chosen by the calling file, not the callee: a strict file calling a coercive library is still strict at its own call sites. Second, the int-to-float widening exception survives strict mode. An int is accepted where a float is declared because the conversion is lossless. Float-to-int is not.

declare(strict_types=1);
function scale(float $x): float { return $x * 2.0; }
scale(3);      // fine - int widens to float
scale('3');    // TypeError - no string coercion in strict mode

readonly and enums, precisely

A readonly property (PHP 8.1) initializes once, and only from inside the declaring class scope. Any later write throws an Error. It is shallow: a readonly property holding an object still lets you mutate that object's internals, since only reassignment of the property is blocked.

Wrong "readonly makes the value deeply immutable." It freezes the binding, not the graph behind it. For a truly immutable value object, hold only scalars or other immutable objects.

Enums (8.1) are singletons. Each case is one shared object, so you compare cases with === and pattern-match them in match. Backed enums add ->value, Suit::from($v) (throws on an unknown value), Suit::tryFrom($v) (returns null), and Suit::cases(). They can hold methods and implement interfaces but carry no per-instance state.

How copy-on-write works, and the foreach-& footgun

Array assignment does not copy immediately. PHP shares one zval and bumps a refcount; the physical duplication happens on the first write (separation). So passing a million-row array into a read-only function costs nothing, while one write inside triggers a full copy. Objects skip this whole dance because the variable holds a handle.

References defeat copy-on-write, and foreach by reference is the classic trap:

foreach ($rows as &$row) { $row['seen'] = true; }
// $row is STILL a live reference to the last element here
foreach ($rows as $row) { /* ... */ }   // this overwrites the last element

The fix is unset($row) right after the reference loop. Interviewers grade this exact bug because it survives code review, passes tests on short inputs, and corrupts only the final element.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

more PHP

was this useful?