PHP interview questions for developers with 4+ years of experience
Advertisement
With four or more years of experience, interviewers stop asking what echo does. They ask how you structure an application, how you would handle a slow job, how you talk to a database safely, and what you do when something breaks in production. The questions below follow the themes that come up most often: design patterns, code organisation, database access, background tasks, parsing XML and JSON, functional versus imperative style, and APIs. The code was run on PHP 8.4. If you need the basics first, start with basic PHP interview questions.
Design and structure
1. Which design patterns do you use in PHP, and can you give examples?
Patterns are named solutions to recurring problems. The ones used daily:
- Factory: create objects without exposing the construction logic.
- Strategy: swap an algorithm at run time behind a common interface.
- Observer: objects subscribe to events (Laravel events, WordPress hooks).
- Repository: hide how data is stored behind a collection-like interface.
- Dependency injection: pass collaborators in instead of creating them inside.
- Singleton: one shared instance (usually better replaced by a container).
- Decorator, Adapter, Facade, Builder, Command: wrap, translate, simplify, assemble step by step, and queue actions.
A strategy plus a factory, in a few lines:
interface Discount { public function apply(float $total): float; }
final class NoDiscount implements Discount { public function apply(float $t): float { return $t; } }
final class Percent implements Discount {
public function __construct(private float $pct) {}
public function apply(float $t): float { return round($t * (1 - $this->pct / 100), 2); }
}
final class DiscountFactory {
public static function for(string $code): Discount {
return match ($code) { 'SAVE10' => new Percent(10), 'HALF' => new Percent(50), default => new NoDiscount() };
}
}
foreach (['SAVE10', 'HALF', 'BOGUS'] as $c) {
echo $c, ": ", DiscountFactory::for($c)->apply(200), "\n";
}
SAVE10: 180
HALF: 100
BOGUS: 200
Adding a new discount means adding a class and one line in the factory; the checkout code never changes. Be ready to say when not to use a pattern: forcing one onto a simple problem is a code smell.
2. What is dependency injection, and why does it matter?
A class receives the objects it needs through its constructor instead of creating them itself. That makes it testable (you can pass a fake) and replaceable (swap the mail service without editing the class).
interface Mailer { public function send(string $to, string $body): bool; }
final class FakeMailer implements Mailer {
public array $sent = [];
public function send(string $to, string $body): bool { $this->sent[] = $to; return true; }
}
final class Welcome {
public function __construct(private Mailer $mailer) {}
public function greet(string $email): bool { return $this->mailer->send($email, "Welcome!"); }
}
$mailer = new FakeMailer();
(new Welcome($mailer))->greet("asha@example.com");
echo count($mailer->sent), " mail(s): ", $mailer->sent[0], "\n"; // 1 mail(s): asha@example.com
A service container (Laravel's, PHP-DI, Symfony's) builds these object graphs automatically from type hints. The related SOLID principle is dependency inversion: depend on interfaces, not concrete classes.
3. How do you organise the code of a project?
Use PSR-4 autoloading with Composer so that the namespace maps to the folder (App\Billing\Invoice lives in src/Billing/Invoice.php), and separate responsibilities into layers: controllers handle HTTP only, services hold business logic, repositories or models talk to the database, and views render output. Keep configuration in environment variables, not in code. Follow PSR-12 for style, and use static analysis (PHPStan or Psalm) and a formatter in CI. For a large codebase, group by feature (billing, orders) rather than by technical type. The principle behind it is single responsibility: one reason to change per class.
{ "autoload": { "psr-4": { "App\\": "src/" } } }
4. What are the SOLID principles in one sentence each?
Single responsibility: a class has one reason to change. Open/closed: extend behaviour without editing existing code. Liskov substitution: a subclass must work wherever its parent does. Interface segregation: many small interfaces beat one large one. Dependency inversion: depend on abstractions. Interviewers like a concrete example from your own code more than the definitions.
Data and performance
5. How do you work with a database connection?
Use PDO (or an ORM built on it) with exceptions enabled, prepared statements for every query with variables, and one connection per request created in one place and injected where needed. Use transactions for multi-step writes, and set the character set explicitly (utf8mb4).
$pdo = new PDO('sqlite::memory:', options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$pdo->exec("CREATE TABLE orders (id INTEGER PRIMARY KEY, customer TEXT, total REAL)");
try {
$pdo->beginTransaction();
$ins = $pdo->prepare("INSERT INTO orders (customer, total) VALUES (?, ?)");
$ins->execute(["Asha", 250.50]);
$ins->execute(["Ravi", 99.00]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
$rows = $pdo->query("SELECT customer, total FROM orders ORDER BY id")->fetchAll();
echo json_encode($rows), "\n";
[{"customer":"Asha","total":250.5},{"customer":"Ravi","total":99}]
Persistent connections (PDO::ATTR_PERSISTENT) can reduce connection time but need care with transactions and state; connection pooling is usually handled outside PHP (ProxySQL, RDS Proxy) or by PHP-FPM settings. See transactions and indexes for the database side.
6. What is the N+1 query problem, and how do you fix it?
Loading a list and then querying inside the loop for each item's related data: 1 query for 100 orders plus 100 queries for their customers. Fix it with a join, a single WHERE id IN (...) query, or eager loading in the ORM (Order::with('customer') in Laravel). Spot it by logging queries per request. See SQL performance optimisation.
7. How do generators help with memory?
A generator yields values one at a time instead of building a whole array, so you can process millions of rows or lines with constant memory.
function readLines(iterable $lines): Generator {
foreach ($lines as $i => $line) {
yield $i + 1 => strtoupper($line);
}
}
function numbers(int $n): Generator { for ($i = 1; $i <= $n; $i++) { yield $i; } }
$before = memory_get_usage();
$sum = 0;
foreach (numbers(1_000_000) as $v) { $sum += $v; }
echo $sum, " ", (memory_get_usage() - $before) < 100_000 ? "low memory" : "high memory", "\n";
foreach (readLines(["a", "b"]) as $n => $l) { echo "$n:$l "; }
echo "\n";
500000500000 low memory
1:A 2:B
Generators are ideal for large files (fgets in a loop), exports, and database cursors.
8. How do you speed up a PHP application?
Measure first (a profiler such as Xdebug or Blackfire, or timing logs). Then, in rough order of payoff: fix slow SQL and N+1 queries; enable OPcache (it caches compiled bytecode and often gives a large speed-up for free); cache expensive results (Redis, Memcached, HTTP caching); use a current PHP version (each release is faster); keep Composer's autoloader optimised (composer dump-autoload -o or --classmap-authoritative); push slow work to background jobs; serve static files from a CDN.
; php.ini, production
opcache.enable=1
opcache.memory_consumption=128
opcache.validate_timestamps=0 ; reset OPcache on each deploy
Background work
9. How do you perform a background task?
A web request should finish quickly, so slow work (sending e-mail, resizing images, generating reports, calling slow APIs) goes to the background. Options:
- Queue + worker: the request pushes a job to a queue (database, Redis, SQS, RabbitMQ), and a long-running worker process executes it. Laravel Queues, Symfony Messenger and Gearman work this way. Workers need retries, failure tracking, timeouts and a supervisor (Supervisor or systemd) to restart them.
- Cron / scheduler: for work that runs on a schedule (
* * * * * php artisan schedule:run). fastcgi_finish_request(): under PHP-FPM, flush the response to the client, then keep running; fine for small things, but not durable.execwith a command in the background: works but gives no retries or monitoring; avoid for important work.
// Laravel
SendInvoice::dispatch($order)->onQueue('mail');
Design jobs to be idempotent (running twice does no harm), because queues deliver "at least once".
10. How do you keep a long-running script from running out of memory or time?
Process in chunks (LIMIT/OFFSET or keyset paging), use generators, free references, avoid loading whole result sets, call gc_collect_cycles() only if profiling shows cycles, and set memory_limit and max_execution_time deliberately for CLI jobs (CLI has no time limit by default). Log progress so a restart can resume.
Data formats and APIs
11. How do you parse and produce JSON, and what can go wrong?
json_encode and json_decode. Use the JSON_THROW_ON_ERROR flag so errors raise exceptions instead of silently returning null.
$json = '{"user":{"name":"Asha","langs":["PHP","SQL"]},"active":true,"score":9.5}';
$data = json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR);
echo $data['user']['name'], " ", $data['user']['langs'][1], " ", var_export($data['active'], true), "\n";
echo json_encode(['name' => 'Zoë', 'price' => 10.0], JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION), "\n";
try {
json_decode('{bad json}', flags: JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
echo "JsonException: ", $e->getMessage(), "\n";
}
Asha SQL true
{"name":"Zoë","price":10.0}
JsonException: Syntax error
Pitfalls: large integers can lose precision on 32-bit systems, invalid UTF-8 makes json_encode fail, and json_decode without associative: true returns stdClass objects. Use the JSON Formatter to inspect a payload by eye.
12. How do you parse XML?
SimpleXML is quickest for small, well-formed documents; DOMDocument with XPath gives more control; XMLReader streams huge files.
$xml = '<catalog><book id="1"><title>PHP</title><price>299</price></book><book id="2"><title>SQL</title><price>199</price></book></catalog>';
$doc = simplexml_load_string($xml);
foreach ($doc->book as $b) {
echo $b['id'], ": ", $b->title, " (", (int) $b->price, ")\n";
}
$titles = array_map('strval', $doc->xpath('//book[price>250]/title'));
echo implode(",", $titles), "\n";
1: PHP (299)
2: SQL (199)
PHP
Never parse XML from untrusted sources with external entities enabled (XXE attacks); modern PHP disables entity loading by default, so don't turn it back on. XML versus JSON: JSON is lighter and maps directly to arrays; XML supports attributes, namespaces, schemas and mixed content, and is still common in SOAP, RSS and some payment and government interfaces.
13. What is the difference between functional and imperative programming?
Imperative code describes how: steps that change variables. Functional code describes what: it composes pure functions (same input, same output, no side effects) with array_map, array_filter and array_reduce, avoiding mutation. PHP supports both; the pragmatic style is imperative structure with functional helpers, and immutable value objects.
$orders = [['id' => 1, 'total' => 250, 'paid' => true], ['id' => 2, 'total' => 99, 'paid' => false], ['id' => 3, 'total' => 400, 'paid' => true]];
// imperative
$sum = 0;
foreach ($orders as $o) { if ($o['paid']) { $sum += $o['total']; } }
// functional
$sum2 = array_sum(array_column(array_filter($orders, fn($o) => $o['paid']), 'total'));
echo $sum, " ", $sum2, "\n"; // 650 650
$compose = fn(callable ...$f) => fn($x) => array_reduce($f, fn($acc, $fn) => $fn($acc), $x);
echo $compose('trim', 'strrev', 'strtoupper')(" abc "), "\n"; // CBA
Pure functions are easy to test and to run in parallel; that is the practical benefit worth mentioning.
14. What is an API, and how do you build and consume one?
An API (application programming interface) lets programs talk to each other; for the web, that usually means a REST API over HTTP returning JSON. Key points: use nouns for resources (/orders/42), HTTP verbs for actions (GET read, POST create, PUT/PATCH update, DELETE remove), correct status codes (200, 201, 204, 400, 401, 403, 404, 422, 429, 500), authentication (API keys, OAuth 2 or JWT), validation, pagination, rate limiting, and versioning (/v1/). To consume an API, use cURL or a client like Guzzle, set timeouts, check the status code, decode with JSON_THROW_ON_ERROR, and handle retries.
I ran a tiny API with PHP's built-in server and called it with cURL:
// api.php (run: php -S 127.0.0.1:8099 api.php)
header('Content-Type: application/json');
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
if ($path === '/v1/orders/42' && $method === 'GET') {
echo json_encode(['id' => 42, 'status' => 'paid']);
} elseif ($path === '/v1/orders' && $method === 'POST') {
$body = json_decode(file_get_contents('php://input'), true);
if (empty($body['customer'])) { http_response_code(422); echo json_encode(['error' => 'customer is required']); return; }
http_response_code(201);
echo json_encode(['id' => 43, 'customer' => $body['customer']]);
} else {
http_response_code(404);
echo json_encode(['error' => 'not found']);
}
function call(string $method, string $url, ?array $payload = null): array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_TIMEOUT => 5, CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
if ($payload !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); }
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
return [$code, json_decode($body, true)];
}
[$c, $b] = call('GET', 'http://127.0.0.1:8099/v1/orders/42'); echo $c, " ", json_encode($b), "\n";
[$c, $b] = call('POST', 'http://127.0.0.1:8099/v1/orders', ['customer' => 'Asha']); echo $c, " ", json_encode($b), "\n";
[$c, $b] = call('POST', 'http://127.0.0.1:8099/v1/orders', []); echo $c, " ", json_encode($b), "\n";
[$c, $b] = call('GET', 'http://127.0.0.1:8099/nope'); echo $c, " ", json_encode($b), "\n";
200 {"id":42,"status":"paid"}
201 {"id":43,"customer":"Asha"}
422 {"error":"customer is required"}
404 {"error":"not found"}
15. How do you secure a PHP application?
Prepared statements against SQL injection, output escaping against XSS, CSRF tokens on state-changing forms, password_hash for passwords, HTTPS and secure cookie flags (HttpOnly, Secure, SameSite), validating and restricting file uploads (check type by content, rename files, store outside the web root), not exposing errors in production (display_errors=0, log them), keeping PHP and dependencies updated (composer audit), least-privilege database users, and secrets in the environment, never in the repository.
16. What do you do about Composer dependencies?
Commit composer.lock so that every environment installs identical versions; use semantic-version constraints (^8.2); run composer audit for known vulnerabilities; deploy with composer install --no-dev --optimize-autoloader; avoid abandoned packages; and wrap third-party code behind your own interface so replacing it is possible.
Quality and operations
17. How do you test PHP code?
Unit tests for logic with PHPUnit or Pest, feature tests for HTTP flows, and a few end-to-end tests for critical paths. Make code testable with dependency injection (see question 2), and run tests in CI on every push. The FakeMailer in question 2 is a test double. A minimal executable expectation looks like this:
final class Calculator { public function vat(float $net, float $rate = 0.18): float { return round($net * $rate, 2); } }
$calc = new Calculator();
assert($calc->vat(100) === 18.0);
assert($calc->vat(250.5, 0.05) === 12.53);
echo "tests passed\n";
(PHP's assert is only active when zend.assertions=1; in practice use PHPUnit. The snippet shows the idea of an executable expectation.) Aim to test behaviour, not implementation details.
18. What is the difference between abstract classes and interfaces, and when do you use traits?
An interface is a contract; an abstract class is a partial implementation; a trait is horizontal code reuse. Prefer interfaces for boundaries between layers, abstract classes when subclasses share real state and behaviour, traits sparingly for small, stateless helpers.
19. How do you handle errors and logging in production?
Convert warnings and notices to exceptions with an error handler, catch what you can handle, and let the rest bubble up to a global handler that logs the exception with context and returns a safe error response. Use PSR-3 loggers (Monolog) with levels, send errors to a central service, never show stack traces to users, and add request ids so you can trace a failure.
20. Tell me about a hard bug you fixed or a system you improved.
Prepare this story. State the symptom, how you measured, what the cause was, what you changed and the result, with numbers if you can ("page time fell from 4 s to 600 ms after removing 200 queries per request"). It shows more about your level than any definition.
How to prepare
Read your own code from the last two years and be ready to defend its choices. Practise the patterns above by writing them from memory. Know one framework deeply (Laravel or Symfony): its container, request lifecycle, queues and ORM. Review SQL design, as covered in normalisation and the top 20 SQL topics, plus the general advice in fundamental questions for software engineering candidates. If you work with Laravel, see how roles and permissions fit into authorisation.