A worker process ingests a 4 GB CSV row-by-row and its RSS climbs steadily even though each row is processed and discarded. The code builds an intermediate object graph where a parent node holds children and each child holds a `$parent` back-reference. `unset()` on the parent at the end of each loop does not reclaim memory. What is the most accurate explanation and fix?
- A. The reference count never reaches zero because of the parent-child cycle, so only PHP's cyclic GC frees it; call gc_collect_cycles() periodically or break the back-reference before unset ✓
- B. unset() is asynchronous in CLI SAPI, so memory is freed only at request shutdown; switch to FPM to force immediate release
- C. The strings are interned in the Zend string table permanently; disable string interning via opcache to free them
- D. Copy-on-write duplicated each row into a new zval that PHP cannot track; add & to the foreach to force a reference and avoid the copy
Correct answer: A. Circular references keep refcounts above zero so simple refcounting can't free them; only the cyclic collector (or manually breaking the cycle) reclaims the memory.
Given `$result = true and false; var_dump($result);` — what is printed and why?
- A. bool(false), because 'and' evaluates both operands before assigning
- B. bool(true), because '=' has higher precedence than 'and', so $result is assigned true first ✓
- C. bool(true), because 'and' short-circuits on the truthy left operand
- D. NULL, because mixing 'and' with '=' is a parse error that yields null
Correct answer: B. '=' binds tighter than the low-precedence 'and', so PHP parses it as `($result = true) and false`, assigning true to $result.
Two traits `A` and `B` both define `public function log()`. A class uses both: `use A, B;` with no conflict-resolution block. What happens?
- A. A fatal error is raised at compile time due to the unresolved method collision unless you use insteadof/as ✓
- B. B::log() silently wins because the later trait in the use list takes precedence
- C. A::log() silently wins because traits are applied left-to-right and the first binding is kept
- D. Both methods coexist and PHP dispatches based on the argument signature (overloading)
Correct answer: A. An unresolved trait method collision is a fatal error; you must disambiguate explicitly with `insteadof` and optionally alias with `as`.
A base class has `public static function create(): static { return new static(); }` and `self` is used elsewhere. A subclass `Admin extends User` calls `Admin::create()`. Which statement about late static binding is correct here?
- A. Because `new static()` uses the runtime-resolved called class, it returns an Admin instance; `new self()` would have returned a User instance ✓
- B. Both `static` and `self` resolve to Admin because static methods always bind to the calling class
- C. `new static()` returns a User instance because static binding is resolved at compile time in the defining class
- D. The call fails because `static` cannot be used as a return type in a factory method
Correct answer: A. Late static binding makes `new static()` use the runtime-called class (Admin), whereas `self` is bound at definition time to User.
You need to stream 10 million rows from a query result, transforming each, without loading them all into memory, AND you must be able to pause/resume the producer cooperatively from a scheduler that also drives network I/O. Which primitive combination is the best fit in PHP 8.1+?
- A. A Generator for lazy row production, driven by a Fiber so the scheduler can suspend it during I/O waits ✓
- B. A single Fiber that returns an array of all 10M transformed rows at the end
- C. An ArrayObject wrapping the full result set with an SplHeap for ordering
- D. pcntl_fork() one child per row and join them via SplQueue
Correct answer: A. Generators give lazy, memory-efficient iteration while Fibers add the cooperative suspend/resume needed for a scheduler juggling I/O, so combining them fits both constraints.
An API endpoint decrements stock: `SELECT qty FROM items WHERE id=?` then `UPDATE items SET qty=qty-1 WHERE id=?` in a transaction under READ COMMITTED. Under high concurrency stock occasionally goes negative. Which fix most directly eliminates the race with minimal contention?
- A. Use `SELECT ... FOR UPDATE` on the row inside the transaction, or make the UPDATE conditional with `WHERE qty > 0` and check affected rows ✓
- B. Switch the isolation level to READ UNCOMMITTED so readers see the latest uncommitted decrement
- C. Wrap both statements in a PHP-level flock() on a lock file shared across web servers
- D. Add an index on qty so the UPDATE locks fewer rows
Correct answer: A. Row-level pessimistic locking with FOR UPDATE (or an atomic conditional UPDATE guarded by affected-row count) closes the check-then-act gap that lets qty go negative.
After migrating a codebase to PHP 8, a previously reliable check `if (strpos($haystack, $needle))` starts treating some matches as misses and, in another spot, `"0" == "0.0"` changed behavior. Which pair of PHP 8 changes explains these?
- A. strpos returning 0 for a front-of-string match was always falsy (a long-standing bug in the code), and in PHP 8 two numeric strings like "0" and "0.0" compare equal with == because both are numeric ✓
- B. strpos now returns false instead of 0 for position 0, and "0"=="0.0" now compares as strings and is false
- C. strpos now throws a TypeError on non-string needles, and numeric-string == comparison was removed in PHP 8
- D. strpos now returns null on no match, and == now always does strict comparison
Correct answer: A. The strpos-at-0 falsy bug predates PHP 8, while PHP 8's comparison change makes number-vs-string use string context only when the string is non-numeric — "0" and "0.0" are both numeric so they compare numerically and are equal.
You pass an object into a function and also an array: `function f($obj, $arr){ $obj->x = 1; $arr[] = 9; }` called as `f($o, $a);`. After the call, what is true about `$o` and `$a`?
- A. $o->x is 1 (the object handle was copied but points to the same instance), and $a is unchanged (arrays are passed by value) ✓
- B. Both $o and $a reflect the mutations because everything in PHP is passed by reference
- C. Neither reflects the mutation because objects and arrays are both copied on pass
- D. $a gains element 9 but $o->x is unchanged because objects are deep-copied on call
Correct answer: A. PHP passes an object's handle by value so the same underlying instance is mutated, while arrays are true value types (copy-on-write) so appends inside the function don't affect the caller's array.
Under OPcache with `opcache.jit=tracing` enabled, a request-heavy JSON API sees almost no speedup after enabling JIT, while a Mandelbrot-style CPU loop got ~4x. What is the most accurate reason?
- A. Typical web request workloads are dominated by I/O and short-lived function calls, so JIT (which mainly accelerates long CPU-bound arithmetic loops) yields little benefit; opcode caching already removed the compile cost ✓
- B. JIT only activates when opcache.jit_buffer_size is 0, so the API's non-zero buffer disabled it
- C. JIT cannot compile code that uses arrays, so the JSON API fell back to the interpreter
- D. JIT requires preloading via opcache.preload; without it the tracing JIT never fires for any workload
Correct answer: A. JIT primarily helps CPU-bound numeric loops; I/O-bound request code gains little because opcode caching already eliminated recompilation and the hot path isn't arithmetic.
A Laravel endpoint renders 50 posts and their authors' names via `$posts = Post::all(); foreach($posts as $p){ echo $p->author->name; }`. The DB log shows 51 queries. Which change fixes the N+1 problem while keeping one query per relation?
- A. Use eager loading: `Post::with('author')->get()`, which issues one query for posts and one WHERE IN query for all authors ✓
- B. Add `->lazy()` to the query so relations are resolved in a single JOIN automatically
- C. Wrap the loop in a transaction so the 50 author queries are batched into one round trip
- D. Enable query result caching so the 50 author queries are served from cache on first request
Correct answer: A. Eager loading with `with('author')` collects the foreign keys and fetches all authors in one WHERE IN query, turning 51 queries into 2.