PHP loops circle the same ground: how a request lives and dies, why FPM falls over under load, why a deploy still serves old code, and how loose comparison lies. Interviewers grade the mechanism behind your answer, not the keyword. This list pulls from the rubric behind our PHP runtime and PHP types modules and from real loop reports.
At a glance, what tends to get asked and where it maps:
| Band | Typical question | Study |
|---|---|---|
| junior | "What happens when a PHP request comes in?" | PHP runtime |
| middle | == vs === and the PHP 8 comparison change | PHP types |
| middle | "I deployed but the site serves old code" | PHP runtime |
| senior | 0e magic-hash auth bypass | Web security |
| senior | all FPM workers busy, nginx returns 502 | HTTP protocol |
The runtime floor
Every PHP question sits on top of one fact: the process forgets everything at the end of the request.
What happens when a request comes in
The web server hands the request to PHP, PHP runs the script top to bottom, produces output, then the interpreter state is thrown away. Superglobals like $_GET, $_POST, and $_SESSION are rebuilt fresh every time. A plain variable does not survive to the next page load because process memory does not persist. This is shared-nothing: persistence lives in a session store, a database, or a cache.
Trap a static property or global assumed to carry state between requests. It holds within one request only. Reaching for a Redis or Memcached layer is the expected answer, because in-process memory dies with the request. The same fact drives per-request database connections: a fresh one each request unless you pool externally (SQL fundamentals).
Study PHP runtime model
Why all your FPM workers can be busy at once
One FPM worker serves one request at a time. Concurrency equals the number of workers, capped by pm.max_children. The classic outage: a slow database or a slow upstream API holds every worker, the queue fills, and nginx starts returning 502 or 504.
Follow-up "How do you size pm.max_children?" Roughly the RAM available for PHP divided by average process RSS, with headroom. pm.max_requests recycles workers to bound leaks. Blaming "traffic" without naming the slow dependency or the worker cap misses the mechanism, and the 504 itself is an HTTP protocol detail: the gateway giving up on a stalled worker.
Study PHP-FPM worker pools
"I deployed but I still see old code"
PHP compiles to opcodes and OPcache stores them in shared memory across the whole pool. In production you set opcache.validate_timestamps=0 so PHP does not stat every file on every request. The cost: changed files are ignored until an explicit opcache_reset() or an FPM reload. Skip that step in your deploy and the old opcodes keep serving.
// WRONG: deploy script swaps the symlink and walks away
ln -sfn /releases/new /var/www/current
// OPcache still holds the previous release's opcodes
// RIGHT: reload FPM (or opcache_reset) after the swap
ln -sfn /releases/new /var/www/current
systemctl reload php-fpm // clears the cache, picks up new files
Preloaded classes (opcache.preload) are stronger still: linked into memory at FPM start, they cannot update without a full restart. opcache.jit=tracing buys almost nothing for I/O-bound web requests (a few percent, sometimes negative) because the bottleneck is the database or the network. Real gains show up only in CPU-bound loops like image processing. Selling JIT as a web-request speedup is a red flag.
Study OPcache and preloading
Where type questions separate people
== vs ===, and what PHP 8 changed
=== compares type and value with no coercion. == coerces first, and that is where the bugs live. The PHP 8 rewrite of string-to-number comparison flipped several long-standing results:
'0' == false // true (both falsy, unchanged)
0 == '' // false in PHP 8, was TRUE in PHP 7
'abc' == 0 // false in PHP 8, was TRUE in PHP 7
'1e3' == '1000' // true (two numeric strings compare as numbers)
The rule to state precisely: when one operand is a number and the other a string, PHP 8 compares as numbers only if the string is numeric, otherwise it casts the number to string. Before 8 the string was cast to a number, so 0 == "foo" was true.
Study Type juggling and comparison
The 0e magic-hash trap
Trap the PHP 8 change did NOT close the magic-hash hole. Two strings like "0e123" and "0e456" are both numeric strings, so they still compare equal under ==.
// WRONG: loose compare on a hash lets a 0e-collision auth bypass through
if (md5($input) == $stored) { grantAccess(); }
// "0e830400451993494058024219903391" == "0e..." is TRUE
// RIGHT: strict compare, or a constant-time check
if (hash_equals($stored, md5($input))) { grantAccess(); }
Auth code that compares hashes with == is exploitable when both sides land on a 0e-prefixed hash. The fix is === or hash_equals, not the version bump. It lives at the seam of type semantics and web security.
Study Web security
PHP 8 features they expect you to write
Constructor promotion, readonly, backed enums, and match each remove a specific pain. Two facts interviewers dig at: match uses === with no fallthrough and throws \UnhandledMatchError when no arm matches, where switch silently does nothing. readonly is shallow, blocking reassignment of the property but not mutation of an object it holds. Enum cases are singletons, so Suit::Hearts === Suit::Hearts, and backed enums give you ::from() (throws on miss) versus ::tryFrom() (returns null). Mapping request payloads onto typed enums is also an API design concern.
Study Enums, readonly, and match
Arrays copy, objects do not
Arrays assign by value through copy-on-write. Objects assign by handle, so two variables point at one object.
$a = [1, 2, 3];
$b = $a; $b[] = 4; // $a is still [1,2,3], the write triggered a copy
$o = new Cart();
$p = $o; $p->add(1); // $o AND $p see the new item, same object
Copy-on-write means passing a large array to a read-only function costs nothing until the first write. Follow-up the foreach ($a as &$v) footgun. After a reference loop, $v stays bound to the last element, so the next foreach ($a as $v) overwrites it. Fix with unset($v).
Study Arrays vs objects
What the grader listens for
Red flags treating a
staticproperty as cross-request storage, "fixing" a busy FPM pool by raisingmax_childrenpast available RAM, claiming PHP 8 killed the magic-hash bypass, and comparing password hashes with==. Any one caps the score no matter how fluent the rest sounds.
The free notes run from the junior floor to the senior internals: PHP runtime and PHP types and OOP. The diagnostic tells you which band you clear.