PHP Coding
Everyday PHP code rests almost entirely on two structures: the array and the string. Neither works the way someone arriving from another language expects. A PHP array is not a list and not a dictionary — it is one structure: an ordered hash map whose keys are ints or strings and whose iteration order equals insertion order. A PHP string is not a sequence of characters but a sequence of bytes, with the encoding recorded nowhere inside it. Most of this topic's traps are derived from those two facts, and the coding round is where they get probed.
On top of the language sits a standard library accumulated over thirty years: functions with inconsistent argument order, loose comparison by default, byte-wise operations sitting next to multibyte ones. Let us name the main traps upfront: array_merge renumbers integer keys while + does not, so merging by id must be done with the union operator; array_filter preserves keys and leaves gaps, which is why json_encode suddenly hands back an object instead of an array; sort returns a bool, not the sorted array; a closure captures its use list by value at creation time, and an arrow function captures automatically — also by value; strlen counts bytes; unserialize on user data is an attack vector. The layer-by-layer breakdown is below.
Topic map
- PHP arrays — the ordered hash map underneath, the
O(1)count, copy-on-write,array_mergeversus+, the keys left behind byarray_filter, and the sorting family. - Function basics — types and
strict_typesat the call boundary, defaults, variadics and the...spread, named arguments, and the PHP 8.5|>operator. - Closures —
Closureas an object,usecapture by value and by reference, arrow functionsfn, and thestrlen(...)syntax. - String functions —
str_containsinstead of thestrpostrap,explodeversuspreg_split,preg_matchversuspreg_match_all, and the PHP 8.5 URI extension. - Multibyte strings — why
strlencounts bytes, howsubstrcuts a letter in half, and what themb_*family does. - Serialization —
json_encodeas an interchange format versusserializeas a snapshot of a PHP object, and whyunserializemust never be called on foreign data. - Streaming I/O — flat memory instead of
file_get_contents, line-by-line reading, generators, and passing a handle rather than a string. - Dependency injection — dependencies in the constructor instead of
newinside, property promotion, the interface over the implementation, and why a service locator is not DI. - Cache-aside — read the cache, fetch the misses from the source, write them back; the batch variant, TTL, invalidation, and the miss stampede.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
| Treating indexed and associative arrays as different structures | It is one type: $a['8'] and $a[8] are one slot — a numeric-string key is cast to int |
Merging arrays keyed by id with array_merge | Integer keys get renumbered — the ids drift; + is what this needs |
Expecting array_filter to reindex | Keys are preserved and gaps remain — and json_encode hands back an object instead of an array |
Writing $sorted = sort($arr) | sort sorts by reference and returns a bool — $sorted ends up holding true |
Applying sort to an associative array | The keys are discarded and renumbered from zero — the data loses its association |
Reading the first key with reset | reset returns a value and moves the internal pointer — the key comes from array_key_first |
Expecting array_walk to return the modified array | It returns a bool and edits the array in place — and only if the parameter is taken by reference |
Capturing with use ($x) and expecting the closure to see later changes | Capture by value happens the moment the closure is created — you need use (&$x) |
| Expecting an arrow function to capture by reference or hold several statements | fn captures by value only and contains exactly one expression |
Writing if (strpos($h, $n)) | A match at offset zero yields 0 — falsy, and the check silently breaks |
Cutting UTF-8 with strlen / substr / ucwords | Bytes rather than characters: double the length, a letter cut in half, mojibake on output |
Passing a regular expression to explode | The separator is taken literally — you need preg_split |
Trusting parse_url to validate a URL | It neither validates nor normalises — PHP 8.5 has Uri\Rfc3986\Uri and Uri\WhatWg\Url for that |
Calling unserialize on data from a cookie or a request body | An object-injection vector: the attacker constructs an object and a __destruct chain |
Expecting json_decode to restore the class | You get a stdClass or an array — JSON carries no class information |
Reading a large file with file() / file_get_contents() | Memory grows with the file, and the worker dies on the limit |
Raising memory_limit instead of streaming | The problem simply moves to the next, bigger file — the buffer is still there |
Constructing a dependency with new inside a method | The dependency is invisible in the signature and the class cannot be isolated in a test |
| Injecting the container and calling it dependency injection | That is a service locator: the dependencies are hidden again, and a missing binding surfaces at runtime |
Computing cache misses against values instead of array_keys($cached) | Hits are never excluded — the source is queried for what the cache already holds |
Interview relevance
This block is tested with a task, not a question — in an editor or at a whiteboard. The interviewer is not watching whether you recalled a function's name but whether you name the trap before it fires: will the keys be renumbered, will filtering leave gaps, will you close the handle, will you reach for mb_* on Cyrillic, will you leave a seam for substituting a dependency.
Typical checks:
- How a PHP array is built and why
count()isO(1). - How
array_mergediffers from+, and when each is the right tool. - What is copied when an array is passed — and when copy-on-write makes a real copy.
- Capture by value versus by reference in a closure; how
fndiffers fromfunction. - Why a string is bytes and where
mb_*is mandatory. - When to use
json_encode, whenserialize, and whyunserializeis dangerous. - How to read a multi-gigabyte file with flat memory.
- Why a constructor beats
newinside — and beats a container inside.
Common wrong answer: "PHP has indexed arrays and associative arrays — they are different things." There is one type, and once that is said correctly, $a['8'] === $a[8], the insertion order of foreach, and the renumbering in array_merge all become explicable. The second classic failure is "ucwords handles UTF-8, the file is UTF-8 after all". The source encoding is irrelevant: ucwords processes bytes and corrupts any multibyte letter — Unicode needs mb_convert_case.