The 60 PHP questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
PHP is a server-side scripting language created by Rasmus Lerdorf in 1994 and designed for building dynamic web pages. Code lives inside <?php ?> tags, can be mixed straight into HTML, and runs through an interpreter on the server, which returns plain HTML to the browser. It's dynamically typed, supports procedural and object-oriented styles, and ships with thousands of built-in functions for strings, arrays, dates, and database access. In interviews, PHP questions probe the type system's loose-comparison rules, PHP's array (an ordered map that doubles as list and dictionary), the object model, and how a request flows from web server to script to response. This page collects the 60 questions that come up most, each with a direct answer and runnable code. For fundamentals, the official PHP manual from The PHP Group is the canonical reference; increasingly the first PHP round runs as a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: PHP Tutorial for Beginners - Full Course | OVER 7 HOURS!
Video: PHP Tutorial for Beginners - Full Course | OVER 7 HOURS! (Envato Tuts+, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your PHP certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
PHP is a server-side scripting language made for the web. Code sits inside <?php ?> tags, runs on the server, and produces HTML that the browser receives. The name stands for PHP: Hypertext Preprocessor, a recursive acronym.
It's used to build dynamic websites and web apps: handling forms, talking to databases, managing sessions and authentication, and generating pages. It backs content platforms like WordPress and frameworks like Laravel and Symfony.
Key point: A one-line definition plus a concrete example (WordPress, form handling) beats a memorized feature list. Interviewers open with this to hear how you organize an answer.
Watch a deeper explanation
Video: PHP Full Course for free 🐘 (Bro Code, YouTube)
Both send output to the browser, and in everyday code they look interchangeable. The differences are small: echo can take multiple comma-separated arguments and returns nothing, while print takes a single argument and returns the integer 1, which means print can sit inside a larger expression where echo can't.
In practice echo is used almost everywhere because it's marginally faster and the multi-argument form is handy. The return value of print is a trivia point that rarely matters day to day.
echo "Hello", " ", "world"; // multiple args, no return
$ok = print "done"; // returns 1
// $ok is now 1Every variable starts with a dollar sign, and you assign with =. There's no separate type declaration; the type comes from the value, so $count = 5 is an int and $name = "Asha" is a string.
Names are case-sensitive, must a letter or underscore after the dollar sign, and can't a digit comes first comes first. PHP is loosely typed, so the same variable can later hold a value of a different type.
$count = 5; // int
$price = 9.99; // float
$name = "Asha"; // string
$active = true; // bool
$count = "now text"; // legal: rebinds to a stringDouble-quoted strings parse the contents: they interpolate variables and expand escape sequences, so "Hi $name\n" inserts the value of $name followed by a newline. Single-quoted strings are almost fully literal, so '$name' prints the raw text dollar-sign-n-a-m-e rather than the variable's value.
Single quotes only interpret \' and \\. They're slightly cheaper because there's no parsing, but the real reason to pick one is intent: use double quotes when you want interpolation, single when you want the literal text.
$name = "Asha";
echo "Hi $name"; // Hi Asha
echo 'Hi $name'; // Hi $name
echo "line\n"; // line + newline
echo 'line\n'; // line\n literallyKey point: The follow-up is usually 'which is faster?'. The honest answer: the difference is negligible; pick by whether you need interpolation.
A PHP array is an ordered map: it stores key-value pairs and remembers insertion order. The same structure works as an indexed list (integer keys) and an associative array (string keys), so PHP doesn't have separate list and dictionary types.
Indexed arrays use auto-incrementing integer keys starting at 0. Associative arrays use string keys you set yourself. Arrays can also nest to build multidimensional structures.
$list = ["a", "b", "c"]; // keys 0, 1, 2
$user = ["name" => "Asha", "age" => 31];
echo $list[0]; // a
echo $user["name"]; // Asha
$grid = [[1, 2], [3, 4]]; // nestedKey point: Saying 'a PHP array is really an ordered map' up front signals you understand the language, not just the syntax.
Watch a deeper explanation
Video: PHP For Beginners | 3+ Hour Crash Course (Traversy Media, YouTube)
== compares values after type juggling, so "1" == 1 and 0 == "0" are both true. === compares value and type together, so "1" === 1 is false because one is a string and the other an int.
The rule the question needs: default to ===. Loose comparison hides bugs, especially around 0, empty strings, and null. PHP 8 tightened some of the worst cases (0 == "abc" is now false), but strict comparison is still the safe habit.
var_dump("1" == 1); // true (type juggling)
var_dump("1" === 1); // false (different types)
var_dump(0 == "0"); // true
var_dump(0 === "0"); // falseLoose (==) vs strict (===) comparison results
How PHP 8 evaluates a few classic pairs. == juggles types; === also checks type.
Key point: Volunteering that PHP 8 changed 0 == "abc" from true to false shows you track the language, not just memorize old gotchas.
Both pull another file's code into the current script. The difference is failure behavior: include emits a warning and keeps running if the file is missing; require throws a fatal error and halts.
Use require for files the script genuinely can't run without (config, class definitions). Use include for optional pieces. The _once variants (include_once, require_once) skip the file if it's already been included, which prevents duplicate-definition errors.
require 'config.php'; // fatal error if missing
include 'sidebar.php'; // warning if missing, continues
require_once 'db.php'; // include only onceSuperglobals are built-in arrays available in every scope without a global declaration. The common ones: $_GET and $_POST for request data, $_SESSION for per-user state, $_COOKIE, $_SERVER for request and environment info, $_FILES for uploads, and $_REQUEST which merges GET, POST, and cookie data.
They're the bridge between the HTTP request and your code. Treat everything in them as untrusted user input: validate and, when it touches a database or HTML, use prepared statements and escaping.
$name = $_POST['name'] ?? ''; // form field, POST body
$id = $_GET['id'] ?? null; // query string ?id=...
$path = $_SERVER['REQUEST_URI']; // requested pathKey point: The interviewer is listening for 'treat superglobals as untrusted'. Mentioning validation in practice is a security signal.
GET sends data in the URL query string, so it's visible, bookmarkable, and length-limited. It should be used for reads that don't change state. POST sends data in the request body, isn't shown in the URL, and suits form submissions that create or change data.
In PHP you read them through $_GET and $_POST. The practical rule matches HTTP semantics: GET for fetching, POST for actions with side effects, and never put passwords or sensitive data in a GET URL.
The concatenation operator is the dot (.), not the plus sign. "Hello" . " " . $name joins strings, and .= appends in place. Using + on strings does arithmetic, coercing numeric strings to numbers.
For building output you can also interpolate inside double quotes, which reads more cleanly than a chain of dots for simple cases.
$name = "Asha";
$greeting = "Hi, " . $name . "!"; // Hi, Asha!
$greeting .= " Welcome."; // append
echo "Hi, $name!"; // interpolationKey point: Mixing up . and + is a classic slip for developers coming from JavaScript. Getting it right without hesitation indicates real PHP experience.
isset() returns true when a variable exists and is not null. empty() returns true when a variable is unset or holds a falsy value (0, "", "0", null, false, empty array). is_null() returns true only when the value is exactly null.
The trap is empty(): it treats 0 and "0" as empty, which bites when 0 is a valid value. For 'does this key exist and have a value', isset() is usually what you want.
| Value | isset() | empty() | is_null() |
|---|---|---|---|
| unset variable | false | true | N/A (warning) |
| null | false | true | true |
| 0 or "0" | true | true | false |
| "text" | true | false | false |
Key point: The 0-is-empty gotcha is the follow-up. Naming it before you're asked shows you've been burned by it.
Use the function keyword, a name, parentheses for parameters, and a body. Parameters can have default values and type declarations, and you can declare a return type after a colon.
PHP passes arguments by value by default, so modifying a parameter inside the function doesn't change the caller's variable unless you prefix the parameter with & to pass by reference.
function greet(string $name, string $greeting = "Hi"): string {
return "$greeting, $name";
}
echo greet("Asha"); // Hi, Asha
echo greet("Ben", "Hello"); // Hello, BenTwo ways: the define() function and the const keyword. const runs at compile time and works inside classes; define() runs at execution time and can build the name dynamically. Constants have no dollar sign and, once set, can't be changed.
For class-level fixed values, class constants (const RATE = 0.2;) are the norm, accessed with the scope resolution operator (ClassName::RATE).
const MAX_USERS = 100;
define("API_URL", "https://example.com");
class Tax {
const RATE = 0.2;
}
echo Tax::RATE; // 0.2The ternary operator condition ? a : b is a compact if/else expression. The short ternary $a ?: $b returns $a if it's truthy, else $b. The null-coalescing operator $a ?? $b returns $a if it exists and is not null, otherwise $b.
The key difference: ?? checks for null and won't warn on undefined array keys or variables, while the short ternary triggers a warning on undefined values and treats any falsy value as the fallback trigger. Use ?? for safe defaults from request data.
$page = $_GET['page'] ?? 1; // 1 if 'page' missing or null
$label = $title ?: 'Untitled'; // 'Untitled' if $title is falsy
$msg = $ok ? 'passed' : 'failed'; // full ternaryType juggling is PHP automatically converting a value's type based on context. In arithmetic, the numeric string "5" becomes the int 5. In a boolean context, 0, "", "0", null, and an empty array are all falsy.
It's convenient for web work where everything arrives as a string, but it causes surprises. The defenses are strict comparison (===) and explicit casting ((int)$value) when a type matters.
echo "5 apples" + 3; // 8 in PHP 7; TypeError-adjacent warning in PHP 8
echo "5" + 3; // 8
var_dump((bool) "0"); // false
var_dump((int) "42px"); // 42Key point: Note that leading-numeric strings like "5 apples" now raise a warning in PHP 8. Knowing the version boundary is the depth signal here.
array_map (transform every element), array_filter (keep elements passing a test), array_reduce (fold to a single value), in_array and array_search (membership), array_keys and array_values, array_merge, sort and its variants, and count.
Reaching for these instead of hand-writing loops is a fluency signal. array_map and array_filter together cover most transform-and-filter work in a couple of readable lines.
$nums = [1, 2, 3, 4];
$doubled = array_map(fn($n) => $n * 2, $nums); // [2,4,6,8]
$evens = array_filter($nums, fn($n) => $n % 2 === 0); // [2,4]
$total = array_reduce($nums, fn($c, $n) => $c + $n, 0); // 10foreach iterates over arrays and iterable objects. foreach ($arr as $value) gives you each value; foreach ($arr as $key => $value) gives you both key and value. It's the idiomatic way to walk an array because it handles both indexed and associative arrays cleanly.
One gotcha: if you take the value by reference (as &$value) to modify elements in place, unset the reference after the loop, or a later reuse of that variable will corrupt the last element.
$user = ["name" => "Asha", "role" => "engineer"];
foreach ($user as $key => $value) {
echo "$key: $value\n";
}
// name: Asha
// role: engineerHistorically PHP reported problems as errors (warnings, notices, fatal errors) handled through error reporting settings, while exceptions are objects you throw and catch with try/catch. Since PHP 7, most fatal errors became Error objects that implement the same Throwable interface as Exception, so you can catch them too.
The practical split: use exceptions for recoverable, expected failure paths in your own code, and let the error system surface programming mistakes. Catch Throwable only at the top level for logging.
try {
$result = 10 / $divisor;
} catch (DivisionByZeroError $e) {
echo "cannot divide by zero";
}htmlspecialchars() converts characters with special meaning in HTML (<, >, &, ") into their entity equivalents. Running user input through it before printing to a page stops that input from being interpreted as markup or script.
It's the core defense against cross-site scripting (XSS). The rule: escape on output, at the point where data enters HTML, and pass ENT_QUOTES so single quotes are handled too.
$comment = $_POST['comment'] ?? '';
echo htmlspecialchars($comment, ENT_QUOTES, 'UTF-8');
// <script> becomes <script> and can't runKey point: Saying 'escape on output' rather than 'sanitize on input' is the more correct framing and interviewers notice it.
For candidates with working experience: OOP, the type system, standard library judgment, and the questions that separate users from understanders.
Classes define properties and methods; the class keyword creates them, new instantiates them, and $this refers to the current instance. PHP supports single inheritance (extends), interfaces (implements), abstract classes, traits for shared behavior, and visibility modifiers public, protected, and private.
Constructors are named __construct(). PHP 8 added constructor property promotion, which declares and assigns properties in the signature, cutting boilerplate. Access static members and constants with the scope resolution operator ::.
class Invoice {
public function __construct(
private float $total,
private string $currency = "USD"
) {}
public function format(): string {
return number_format($this->total, 2) . " " . $this->currency;
}
}
$inv = new Invoice(199.5);
echo $inv->format(); // 199.50 USDKey point: Using constructor property promotion in your example signals you write modern PHP 8, not PHP 5 habits.
Watch a deeper explanation
Video: Full PHP 8 Tutorial - Learn PHP The Right Way (Program With Gio, YouTube)
These three keywords control where a property or method can be reached. public members are accessible from anywhere, inside or outside the class. protected members are accessible within the declaring class and any subclass that extends it. private members are accessible only inside the declaring class itself, not even in its subclasses.
The convention is to keep properties private or protected and expose behavior through methods, so internal state can change without breaking callers. This is encapsulation, and it's what interviewers are checking when they ask.
| Modifier | Same class | Subclass | Outside |
|---|---|---|---|
| public | Yes | Yes | Yes |
| protected | Yes | Yes | No |
| private | Yes | No | No |
An abstract class can't be instantiated and can mix implemented methods with abstract ones that subclasses must define; a class can extend only one. An interface declares method signatures (and since PHP 8 constants) with no implementation, and a class can implement many.
Use an abstract class to share partial implementation up a single hierarchy. Use an interface to declare a capability that unrelated classes can promise. Modern PHP often reaches for interfaces plus traits over deep abstract hierarchies.
interface Exportable {
public function toArray(): array;
}
abstract class Report implements Exportable {
abstract public function rows(): array;
public function count(): int { return count($this->rows()); }
}A trait is a bundle of methods (and properties) you can mix into multiple classes with use, giving horizontal code reuse without inheritance. It solves the problem that PHP has single inheritance but you sometimes need to share behavior across unrelated classes.
The caution: traits can hide where behavior comes from and cause method-name collisions, which PHP resolves with insteadof and as. Reach for a trait when the shared code doesn't fit a clean 'is-a' relationship.
trait Timestampable {
public ?string $createdAt = null;
public function touch(): void {
$this->createdAt = date('c');
}
}
class Post {
use Timestampable;
}static members belong to the class itself, not any instance. A static property is shared across all instances; a static method runs without an object and can't use $this. Access them with the scope resolution operator: ClassName::method() or self:: inside the class.
They suit counters, factory methods, and utility functions. Overuse turns into global state that's hard to test, so the intermediate-level coverage names that trade-off.
class Counter {
private static int $count = 0;
public static function bump(): int {
return ++self::$count;
}
}
Counter::bump(); // 1
Counter::bump(); // 2Magic methods are hooks PHP calls automatically at certain moments; their names double underscores comes first. __construct and __destruct handle setup and teardown. __get and __set intercept access to inaccessible properties. __call and __callStatic intercept undefined method calls. __toString controls how an object prints. __invoke lets an object be called like a function.
They power dynamic behavior in ORMs and frameworks. The caution: they make code less explicit and slower, so use them deliberately, not as a default.
class Config {
private array $data = [];
public function __get($key) { return $this->data[$key] ?? null; }
public function __set($key, $value) { $this->data[$key] = $value; }
}
$c = new Config();
$c->timeout = 30; // routes through __set
echo $c->timeout; // routes through __getPDO (PHP Data Objects) is a database access layer that works across many database engines with one API, so switching from MySQL to PostgreSQL means changing the connection string, not the query code. mysqli is MySQL-only.
Both support prepared statements, which is the security-relevant part. PDO's consistent interface and named parameters make it the common default for new code. mysqli still appears in MySQL-specific or legacy projects.
$pdo = new PDO('mysql:host=localhost;dbname=app', $user, $pass);
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);Key point: 'Why PDO over mysqli?' almost always follows. Lead with database portability and named parameters.
Watch a deeper explanation
Video: PHP For Absolute Beginners | 6.5 Hour Course (Traversy Media, YouTube)
A prepared statement sends the SQL structure to the database first, with placeholders where data goes, then sends the values separately. Because the query is parsed before the data arrives, user input can't change the query's structure. It's treated purely as data, never as SQL.
That separation is exactly what defeats SQL injection. String-concatenating user input into a query is the vulnerability; binding parameters removes it. This is the single most repeated PHP security question, so The technical detail be automatic.
// vulnerable: never do this
$sql = "SELECT * FROM users WHERE name = '$name'";
// safe: value can't alter the query
$stmt = $pdo->prepare('SELECT * FROM users WHERE name = ?');
$stmt->execute([$name]);Key point: the question needs the mechanism (SQL and data sent separately), not just 'it's safer'. Explain why and you've answered the follow-up.
The core set: cross-site scripting (XSS), fixed by escaping output with htmlspecialchars; cross-site request forgery (CSRF), fixed with per-form tokens checked on submit; and session hijacking, reduced by regenerating the session ID on login and using secure, HttpOnly cookies.
Also password storage with password_hash() (never plain text or raw md5), input validation, and keeping error display off in production so stack traces don't leak. these is useful because marks a candidate who's shipped real software.
Use password_hash(), which applies a strong, salted, adaptive algorithm (bcrypt by default, Argon2 available) and encodes the salt and cost into the result. Verify with password_verify(), which extracts the parameters and compares safely. Never store plain text, and never use plain md5 or sha1 for passwords.
The adaptive cost matters: it lets you increase work as hardware gets faster, and password_needs_rehash() tells you when to upgrade an existing hash on the user's next login.
$hash = password_hash($password, PASSWORD_DEFAULT);
// store $hash
if (password_verify($input, $hash)) {
// login succeeds
}A namespace groups classes, functions, and constants under a name to avoid collisions between libraries, declared with namespace App\Models; at the top of a file. You reference names with a backslash path or import them with use.
Autoloading loads class files on demand instead of requiring each one. Composer's PSR-4 autoloader maps namespaces to directories, so referencing App\Models\User loads src/Models/User.php automatically. That's why modern PHP projects rarely write manual require lines.
namespace App\Models;
class User {
public function __construct(public string $name) {}
}
// elsewhere
use App\Models\User;
$u = new User('Asha');Composer is PHP's dependency manager. composer.json declares your project's packages; composer install reads it, resolves versions, downloads them into vendor/, and generates an autoloader. composer.lock pins exact versions so every machine and CI build gets the same dependency tree.
It also standardized PSR-4 autoloading across the ecosystem, which is why require statements largely disappeared from application code. Knowing the lock-file discipline (commit it for applications) is the mark of production experience.
composer require monolog/monolog # add a package
composer install # install from lock file
# then in code:
# require 'vendor/autoload.php';PHP supports scalar type hints (int, float, string, bool), array, object, class and interface names, nullable types (?int), union types (int|string, since 8.0), and return types after a colon. Typed properties (public int $count) arrived in 7.4.
declare(strict_types=1) at the top of a file turns off type coercion for that file's function calls, so passing a string where an int is declared throws a TypeError instead of silently converting. Strict types is the recommended default for new code.
declare(strict_types=1);
function total(int|float $a, int|float $b): float {
return (float) ($a + $b);
}
total(2, 3); // 5.0
total("2", 3); // TypeError under strict_typesmatch (PHP 8.0) is an expression that returns a value, compares with strict equality (===), needs no break, and throws if no arm matches (with no default). switch is a statement, uses loose comparison, falls through without break, and doesn't return a value.
match is safer and terser for mapping an input to a result. Reach for switch only when you genuinely want fall-through or need to run several statements per case.
$label = match ($status) {
200, 201 => 'Success',
404 => 'Not Found',
500 => 'Server Error',
default => 'Unknown',
};Key point: Naming the three real differences (returns a value, strict comparison, no fall-through) is what separates a memorized answer from understanding.
Arrow functions (fn($x) => $x * 2, since PHP 7.4) are a short single-expression syntax that automatically captures variables from the enclosing scope by value, so you don't write a use clause. A regular closure (anonymous function) can hold multiple statements but must list captured variables explicitly with use.
Use arrow functions for the small callbacks passed to array_map and array_filter. Reach for a full closure when the body needs more than one expression or you must capture by reference.
$factor = 3;
$scale = fn($n) => $n * $factor; // captures $factor automatically
$scale(5); // 15
$scale2 = function ($n) use ($factor) { return $n * $factor; };A generator is a function that uses yield to produce values one at a time instead of building and returning a whole array. Each iteration resumes right after the last yield, so memory stays flat no matter how many values you produce. It's the tool for streaming large files or infinite sequences.
You iterate a generator with foreach just like an array, but it holds only one value in memory at a time. yield from delegates to another iterable inside a generator.
function readLines(string $path): Generator {
$handle = fopen($path, 'r');
while (($line = fgets($handle)) !== false) {
yield rtrim($line);
}
fclose($handle);
}
foreach (readLines('huge.log') as $line) {
// one line in memory at a time
}By default PHP passes arguments by value, so a function works on a copy and can't change the caller's variable. Objects are a common point of confusion: the object handle is passed by value, but that handle points to the same object, so property changes are visible outside. It's not the same as passing the variable by reference.
Prefix a parameter with & to pass a real reference, which lets the function reassign the caller's variable. Use it sparingly; functions that mutate their inputs are harder to reason about than functions that return new values.
function addTax(float &$price): void {
$price *= 1.2; // changes caller's variable
}
$amount = 100.0;
addTax($amount);
echo $amount; // 120Key point: The object-handle nuance is the trap. Being able to say 'objects are passed by handle, not by reference' shows real understanding.
A variadic function collects any number of trailing arguments into an array using ...$args in the signature. The same ... operator spreads an array into individual arguments at a call site, and since PHP 8.1 it can spread string-keyed arrays into named arguments.
This replaced the older func_get_args() approach with something typed and explicit. It's handy for wrappers that forward arguments and for functions that take a variable-length list.
function sum(int ...$nums): int {
return array_sum($nums);
}
sum(1, 2, 3); // 6
$values = [4, 5, 6];
sum(...$values); // 15 (spread)json_encode() turns PHP values into a JSON string; json_decode() parses JSON back. Pass true as the second argument to json_decode to get associative arrays instead of stdClass objects. Since PHP 7.3 you can pass JSON_THROW_ON_ERROR so bad input throws instead of returning null silently.
The common bug is not checking for failure: without the throw flag, a decode error returns null and you carry a null downstream. Objects need __serialize or JsonSerializable to control their JSON shape.
$data = ['name' => 'Asha', 'roles' => ['admin', 'user']];
$json = json_encode($data);
$back = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
echo $back['name']; // AshaThe object-oriented DateTime and immutable DateTimeImmutable classes are the modern approach: they parse, format, and do arithmetic with clear methods, and DateTimeImmutable avoids the bug where modifying a date accidentally changes another reference to it. DateTimeZone handles time zones explicitly.
The older procedural date() and strtotime() still appear everywhere and are fine for simple formatting. For anything involving arithmetic or time zones, prefer DateTimeImmutable.
$now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
$later = $now->modify('+2 days');
echo $later->format('Y-m-d H:i');
// $now is unchanged: immutability prevents aliasing bugsadvanced rounds probe internals, design judgment, and production scars. Expect every answer here to draw a follow-up.
The web server (Nginx or Apache) receives the HTTP request and hands PHP work to a process manager, usually PHP-FPM. FPM assigns a worker, which bootstraps the script: it parses and compiles the PHP to opcodes (cached by OPcache after the first run), populates the superglobals, runs your code, and returns the output as the response body. Then the worker resets and waits for the next request.
The point interviewers probe is shared-nothing: each request starts with fresh memory and no leftover state from prior requests, which is why long-lived state must live in a database, cache, or session store. That model shapes how you architect a PHP app.
A PHP request, end to end
The shared-nothing reset is the whole point. External stores hold anything that must persist between requests.
Key point: This is really an architecture question. Landing 'shared-nothing, so state lives externally' is what The production-ready answer sounds like.
PHP compiles source to opcodes on every request by default, which is wasted work when the code hasn't changed. OPcache stores the compiled opcodes in shared memory so subsequent requests skip parsing and compilation. On a busy app this is a large, cheap win, and it ships enabled in modern PHP.
The senior-level extras: opcache.validate_timestamps controls whether it re-checks files for changes (turn it off in production and clear the cache on deploy), and preloading (PHP 7.4+) can load framework classes into memory once at startup for a further gain.
PHP uses reference counting: every value tracks how many references point to it, and when the count reaches zero the memory is freed immediately. A cycle collector supplements this to reclaim reference cycles that counting alone can't free, since two objects pointing at each other never reach a zero count.
Because each request is short-lived and its memory is wiped at the end, leaks matter far less than in a long-running process. They still bite in CLI workers and long-running scripts, where you watch for unbounded arrays and circular references and can call gc_collect_cycles() explicitly.
PSRs are recommendations from the PHP-FIG group that let independent libraries work together. PSR-4 defines autoloading (namespace-to-path mapping), PSR-12 defines coding style, PSR-7 defines HTTP message interfaces, PSR-11 defines a container interface, and PSR-15 defines HTTP middleware.
They're the reason a Symfony component drops into a Laravel app and a PSR-7 request object works across frameworks. Citing the specific ones you use signals you work in the modern ecosystem rather than isolated legacy code.
Dependency injection means a class receives its collaborators from outside (usually through the constructor) instead of creating them itself. That inverts control: the class declares what it needs, and a container wires it up. The payoff is testability, since you can pass in a fake database or mailer in tests.
Frameworks like Symfony and Laravel ship a DI container that reads type hints and autowires dependencies. The senior framing is coupling: DI decouples a class from concrete implementations so you can swap them, which is what makes large codebases maintainable.
class OrderService {
public function __construct(
private PaymentGateway $gateway,
private Mailer $mailer
) {}
// dependencies passed in, not newed up: easy to test
}PHP 8.1 introduced enums (real enumerated types, backed or pure, with methods), readonly properties (assignable once then immutable), fibers (a low-level concurrency primitive), and first-class callable syntax. PHP 8.2 added readonly classes and disallowed dynamic properties by default, which catches typo bugs.
The practical shift: enums replace class constants for fixed sets of values, and readonly properties model value objects that can't be mutated after construction. Both make invalid states unrepresentable, which is a real design improvement.
enum Status: string {
case Draft = 'draft';
case Published = 'published';
public function label(): string {
return ucfirst($this->value);
}
}
Status::Published->label(); // PublishedLate static binding lets a static reference resolve to the class that was actually called at runtime, not the class where the method is written. self:: is fixed to the defining class, while static:: follows the calling class. It fixes the case where a parent method needs to instantiate or reference the child that invoked it.
The classic use is a static factory in a base class that should return an instance of whichever subclass called it. Without static::, self::new() would always build the base type.
class Model {
public static function create(): static {
return new static(); // resolves to the calling class
}
}
class User extends Model {}
$u = User::create(); // instance of User, not ModelKey point: Contrasting self:: (defining class) with static:: (calling class) in one sentence is the crisp answer here.
When two traits used in the same class define a method with the same name, PHP raises a fatal error unless you resolve it. The insteadof operator picks which trait's version wins, and the as operator aliases the other under a new name so both stay reachable. as can also change a method's visibility.
This is the escape hatch that keeps traits usable when they overlap. Needing it often, though, is a sign the traits are doing too much and a redesign toward composition would read better.
trait A { public function hello() { return 'A'; } }
trait B { public function hello() { return 'B'; } }
class C {
use A, B {
A::hello insteadof B;
B::hello as helloB;
}
}Declare properties readonly (PHP 8.1) or make the whole class readonly (8.2), assign them once in the constructor, and add no setters. Any 'change' returns a new instance with the updated field rather than mutating the current one, the wither pattern. Combine with strict types so the fields can't drift.
Value objects model concepts like Money or an EmailAddress where equality is by value and the object should never change after creation. They remove a class of aliasing bugs and make code easier to reason about.
final class Money {
public function __construct(
public readonly int $cents,
public readonly string $currency
) {}
public function add(Money $other): self {
return new self($this->cents + $other->cents, $this->currency);
}
}N+1 happens when you fetch a list of records with one query, then run one more query per record to load a relation, so displaying 100 posts with their authors fires 101 queries. It's the most common performance bug in ORM-backed PHP apps because the extra queries are hidden behind lazy-loaded relations.
The fix is eager loading: fetch the related data up front in one or two queries, typically with a join or an IN clause (Eloquent's with(), Doctrine's fetch join). Spotting it requires looking at the actual query log, which is why query counting in tests is worth adding.
// N+1: one query for posts, then one per post for its author
$posts = Post::all();
foreach ($posts as $post) { echo $post->author->name; }
// fixed: eager load authors in a second query
$posts = Post::with('author')->get();Layer it. OPcache caches compiled opcodes at the runtime level for free. An in-memory store like Redis or Memcached caches expensive query results and computed data across requests, since PHP's shared-nothing model wipes per-request memory. HTTP caching (ETag, Cache-Control) pushes responses to the browser and CDN.
The hard part isn't setting a value, it's invalidation: deciding when cached data is stale. A common pattern is cache-aside with a TTL plus explicit invalidation on writes. Naming the invalidation problem is what marks The production-ready answer.
The default model is synchronous and shared-nothing, one request per worker, and that's fine for most web apps because the web server runs many workers in parallel. For concurrency inside a script, PHP 8.1 added fibers (a low-level primitive), and event-loop runtimes like ReactPHP, Amp, and Swoole enable async I/O and long-running processes.
The pragmatic answer: most PHP concurrency is horizontal (more FPM workers) plus a queue system (like a message broker with worker processes) for background jobs. Reach for the async runtimes when you specifically need many concurrent connections or persistent processes, such as WebSockets.
PHPUnit is the standard: test classes with test methods, assertions, data providers for input matrices, and mocks or test doubles for external boundaries like the database and network. Pest is a popular newer layer with a lighter syntax on top of PHPUnit. Structure tests as a pyramid: many fast unit tests, fewer integration tests, a handful of end-to-end.
The judgment interviewers probe: test behavior through public interfaces rather than private internals, and inject dependencies so you can substitute fakes. Add query-count assertions to catch N+1 regressions and use a transaction rollback per test to keep the database clean.
use PHPUnit\Framework\TestCase;
class DiscountTest extends TestCase {
public function test_applies_percentage(): void {
$this->assertSame(90.0, applyDiscount(100.0, 10));
}
}Turn display_errors off so stack traces never reach users, set a strict error_reporting level, and convert errors to exceptions with a global handler so nothing fails silently. Log through a structured logger (Monolog is the standard) to a central destination, with context attached to each entry.
Register set_exception_handler and set_error_handler at bootstrap to catch anything uncaught, return a clean error response, and log the detail. The anti-pattern to name is swallowing exceptions with an empty catch, which turns failures into silent data corruption.
set_exception_handler(function (Throwable $e) use ($logger) {
$logger->error($e->getMessage(), ['exception' => $e]);
http_response_code(500);
echo 'Something went wrong.';
});Assigning an object ($b = $a) copies the handle, so both variables point to the same object. The clone keyword makes a copy of the object itself, but that copy is shallow: nested objects are still shared between original and clone.
To make nested objects independent, implement __clone(), which PHP calls on the new copy right after cloning, and clone the inner objects there. This is the object-graph version of the shallow-versus-deep copy distinction.
class Order {
public function __construct(public Address $shipTo) {}
public function __clone() {
$this->shipTo = clone $this->shipTo; // deep-copy the inner object
}
}Without it, PHP coerces scalar arguments to match declared types (passing "5" to an int parameter becomes 5). With strict_types=1, a mismatch throws a TypeError instead; you must pass the exact declared scalar type, with int-to-float widening the one allowed exception.
The subtlety: strict_types applies per file, and specifically to the file where the call is made, not where the function is defined. So enabling it in your file governs how your calls behave, which is why it belongs at the top of every source file in a strict codebase.
Key point: The per-file, call-site scope is the trap. Saying 'it applies to the file making the call, not the definition' is the senior-level detail.
Clarify scope first (auth model, versioning, expected load), then layer it: a thin routing and controller layer that only handles HTTP, a service layer with the business logic, and a repository layer for data access. Validate and deserialize input at the edge, return JSON with correct status codes, and keep controllers free of database calls so the logic is testable.
Cross-cutting concerns go in middleware: authentication (token or session), rate limiting, CORS, and logging. Version the API in the URL or a header, use prepared statements throughout, paginate list endpoints, and return consistent error shapes. Structuring the answer clarify, layers, cross-cutting, edges is what the question scores.
// controller stays thin: HTTP in, HTTP out
public function show(int $id): JsonResponse {
$user = $this->users->find($id); // repository
if (!$user) {
return new JsonResponse(['error' => 'Not found'], 404);
}
return new JsonResponse($user->toArray());
}Scale horizontally first, since the shared-nothing model makes it natural: run more PHP-FPM workers and more servers behind a load balancer, because nothing is held in local memory between requests. Move sessions to a shared store like Redis so any server can serve any user, and put static assets on a CDN.
Then attack the real bottlenecks in order: the database (indexes, read replicas, query fixes for N+1), then caching layers (OPcache, an object cache, HTTP caching), then offloading slow work to a queue with background workers. The senior point is that PHP itself rarely limits you first; the database and I/O do, so measure before you scale anything.
Key point: Leading with 'shared-nothing makes horizontal scaling easy, and the database is usually the real limit' is the answer that indicates production experience.
PHP wins when you want a language purpose-built for the request-response web, cheap hosting everywhere, and a deep ecosystem of mature frameworks and content platforms. Each request starts with a clean slate (the shared-nothing model), which sidesteps a whole class of shared-state bugs but also means PHP leans on external stores for long-lived state. Where it competes closely with Node.js is real-time and streaming work, where a persistent event loop fits better. Knowing these trade-offs out loud is itself an interview signal: it shows you pick tools on merits, not habit.
| Language | Concurrency model | Best at | Watch out for |
|---|---|---|---|
| PHP | Shared-nothing per request | CMS, server-rendered web apps, quick hosting | Long-lived state, real-time streams |
| Node.js | Single-thread event loop | Real-time apps, JSON APIs, one language front to back | CPU-bound work blocking the loop |
| Python | Threads plus the GIL | Data, ML, scripting alongside web | Raw web throughput, GIL for CPU parallelism |
| Ruby | Threads plus a GIL (MRI) | Convention-driven web apps (Rails) | Smaller talent pool, runtime speed |
Prepare in layers, and practice out loud. Most PHP rounds move from concept questions to live coding to a design or debugging discussion, so rehearse each stage rather than only reading answers.
The typical PHP interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These PHP questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works
Q20. How do you write comments in PHP?
PHP supports single-line comments with // or #, and multi-line comments between /* and */. Documentation blocks use /** ... */ with @param and @return tags, which tools and editors read for type hints and API docs.
The docblock style is worth mentioning because it feeds IDE autocompletion and static analysis, which matters on real teams.