Free Handbook · Every example compiled & verified

Interview Questions

60 PHP interview questions with model answers, from == versus === to Laravel, security and scaling, plus a coding round walkthrough and take-home checklist.

0 / 148 lessons🔥 0 day streak
ShareXLinkedIn

Module 15 · what you'll be able to do

  • Answer the 25 junior PHP questions that decide first-round screens, precisely and with examples
  • Explain memory, generators, closures, DI containers, sessions and OPcache at the depth a mid-level round expects
  • Reason through senior questions on architecture, scaling, upgrades, webhooks, migrations and multi-tenant security
  • Run a live coding round with a repeatable script, and hand in a take-home that reviewers approve
01

How to use this module

PHP interviews follow a stable pattern: a screen on the language (comparison rules, types, arrays, OOP), a technical round on security, databases and a framework (usually Laravel or Symfony), and for experienced roles a design conversation about scaling, queues and production. The 60 questions below are grouped the same way: 25 junior, 25 mid-level, 10 senior.

  • Say the answer out loud before opening it. Recognising an answer when you read it is not the same as producing it under pressure.
  • Read the "what they are really testing" line. It tells you what the interviewer will follow up on, which is where most candidates lose points.
  • Back every claim with code you have run. The modules these draw on, such as Fundamentals, Arrays and Web Basics, have examples you can paste into your editor.
Say which PHP version
Many companies still run PHP 7.4 or 8.1 in production while new work targets 8.3 or later. Say which version a feature arrived in ("enums, PHP 8.1"; "property hooks, PHP 8.4"). It shows you follow the language and avoids suggesting something their servers cannot run.
02

Junior: the language and the runtime (10 questions)

Asked in nearly every first-round PHP screen. They are short questions with precise answers, and a vague answer here ends the interview early. Background: Module 01, Module 03 and Module 05.

JuniorHow does a PHP request run, from the browser to the response?

The web server (Nginx or Apache) receives the HTTP request and hands PHP files to PHP-FPM, a pool of PHP worker processes (or to mod_php inside Apache). A worker compiles the script to opcodes (cached by OPcache so later requests skip compilation), executes it with the superglobals ($_GET, $_POST, $_SERVER) filled from the request, and sends whatever the script echoes back as the response body. When the script ends, everything it created is thrown away: PHP is shared-nothing, so each request starts from a clean slate. State that must survive lives in a database, Redis, files or the session.

What they are really testing: Whether they know PHP is request-scoped and shared-nothing, and what OPcache and FPM are for.

JuniorWhat is the difference between == and ===?

=== is strict: true only if both values have the same type and the same value. == is loose: it converts types first. Since PHP 8, comparing a number with a non-numeric string converts the number to a string, so 0 == "a" is false (it was true in PHP 7). Loose comparison still surprises: "1" == "01" and "10" == "1e1" are true because both sides are numeric strings, and null == false is true. Use === by default, and pass true as the strict flag to in_array and array_search.

What they are really testing: That they default to strict comparison and know the PHP 8 change to string-number comparison.

JuniorWhat are PHP's data types?

Scalar: int, float, string, bool. Compound: array, object, callable, iterable. Special: null and resource (an external handle such as an open file, being replaced by objects in modern PHP). Type declarations add mixed, void, never, nullable ?int, unions int|string, intersections A&B and false/true literal types. get_debug_type($x) names a value's type, and var_dump shows type and value together.

What they are really testing: A complete, current list including the PHP 8 type-system additions.

JuniorWhat does declare(strict_types=1); do?

By default PHP coerces scalar arguments to the declared parameter type: passing "5" to function f(int $n) silently becomes 5. With declare(strict_types=1); as the first statement of a file, calls made from that file must pass the exact scalar type (the only allowed widening is int to float), otherwise a TypeError is thrown. It is per-file and applies to the caller, not the function definition. Most modern codebases put it in every file.

What they are really testing: That it is per-file and caller-side, which is the detail most candidates get wrong.

JuniorWhat is the difference between single-quoted and double-quoted strings?

Double-quoted strings interpolate variables ("Hi $name", "{$user->name}", "{$items[0]}") and understand escapes like \n and \t. Single-quoted strings are literal: only \' and \\ are escapes, so '\n' is two characters. Heredoc (<<<EOT) behaves like double quotes over several lines; nowdoc (<<<'EOT') like single quotes. The speed difference is negligible; pick by whether you need interpolation.

What they are really testing: Basic fluency, plus heredoc/nowdoc as the follow-up.

JuniorWhat is the difference between isset, empty and is_null?

isset($x) is true if the variable exists and is not null; it never warns about undefined variables or keys. empty($x) is true if the value is missing or falsy: null, false, 0, 0.0, "", "0" and [], which makes empty("0") a classic bug for form fields. is_null($x) is true only for null and warns if $x is undefined. To check that an array key exists even when its value is null, use array_key_exists. In modern code, $x ?? $default replaces most isset ternaries.

What they are really testing: The "0" trap in empty() and the null-value trap in isset().

JuniorHow do you pass variables by value and by reference?

Scalars and arrays are passed by value: the function gets a copy (copy-on-write, so the copy is only made if it is modified). Prefix the parameter with & to pass by reference: function addOne(array &$a) { $a[] = 1; }. Objects are passed as handles: the function can change the object's properties, but assigning a new object to the parameter does not affect the caller. A foreach ($items as &$item) loop leaves $item as a reference afterwards, so unset($item) after the loop, or a later loop will overwrite the last element.

What they are really testing: Copy-on-write, object handles, and the foreach-by-reference bug.

JuniorWhat is the difference between include, require and their _once versions?

All four load and run another PHP file. If the file is missing, include emits a warning and continues, while require throws a fatal error and stops. The _once versions skip a file that was already loaded, which prevents "cannot redeclare function" errors. In modern PHP you rarely write these by hand: Composer's autoloader (require 'vendor/autoload.php';) loads each class on first use via PSR-4, and require is left for config files that return an array.

What they are really testing: That they know autoloading replaced manual includes.

JuniorWhat is the null coalescing operator and the nullsafe operator?

$a ?? $b returns $a if it exists and is not null, otherwise $b, without warning on undefined keys: $page = $_GET['page'] ?? 1;. ??= assigns only when the left side is null or missing. The nullsafe operator ?-> (PHP 8.0) short-circuits a chain when a link is null: $user?->address?->city returns null instead of throwing "Attempt to read property on null". Neither is the same as ?:, which falls back on any falsy value, including 0 and "".

What they are really testing: The difference between ?? (null) and ?: (falsy).

JuniorWhat are the differences between echo, print, print_r and var_dump?

echo and print output strings; echo takes several arguments and returns nothing, print takes one and returns 1. print_r prints arrays and objects in a readable form (pass true to return it as a string). var_dump shows the type and value, including bool(false) and NULL, which print_r shows as empty. var_export prints valid PHP code. For debugging, var_dump is the honest one; in frameworks, dump()/dd(), and for real debugging, Xdebug with breakpoints.

What they are really testing: Knowing which one reveals types, and that Xdebug exists.

03

Junior: object-oriented PHP (8 questions)

Interviewers probe whether you use classes and interfaces as design tools or only as places to put functions. Examples win over definitions. Background: Module 06 and Module 07.

JuniorWhat are the four pillars of OOP, with a PHP example of each?

Encapsulation: private properties changed only through methods that keep the object valid (private int $balance, deposit() rejects negatives). Inheritance: class Admin extends User reuses and specialises behaviour. Polymorphism: code written against an interface (PaymentGateway) works with any implementation (StripeGateway, FakeGateway). Abstraction: an interface or abstract class exposes what an object does and hides how. The best answers add when not to inherit: prefer composition for "has-a" relationships.

What they are really testing: Examples rather than definitions, and awareness of composition over inheritance.

JuniorWhat is the difference between an interface, an abstract class and a trait?

An interface is a contract: method signatures and constants, no state; a class can implement many. An abstract class can hold state, constructors and implemented methods alongside abstract ones; a class extends only one. A trait is copy-paste reuse: its methods and properties are pasted into each class that uses it, with no type relationship (instanceof a trait is impossible). Use interfaces for types, abstract classes for a shared base with real behaviour, and traits sparingly for cross-cutting helpers.

What they are really testing: That they know traits are not types, and single inheritance versus multiple interfaces.

JuniorWhat do public, protected and private mean?

public: accessible from anywhere. protected: from the class and its subclasses. private: from the declaring class only (not even subclasses). Visibility applies to properties, methods and constants. PHP 8.1 added readonly properties (assigned once, inside the class), and PHP 8.4 added asymmetric visibility such as public private(set) string $name, readable everywhere but writable only inside the class. Default to private and widen only when needed.

What they are really testing: Correct definitions, plus modern readonly and asymmetric visibility.

JuniorWhat is the difference between self, static and $this?

$this is the current object instance, only available in non-static methods. self:: refers to the class where the code is written. static:: refers to the class that was actually called at run time (late static binding). So in a parent's public static function create(): static { return new static(); }, calling Child::create() returns a Child, while new self() would return the parent. Factories and fluent builders rely on static.

What they are really testing: Late static binding, the classic follow-up.

JuniorWhat are magic methods? Name the common ones.

Methods PHP calls automatically, all starting with two underscores. __construct and __destruct; __toString (string conversion); __get, __set, __isset, __unset (inaccessible properties); __call and __callStatic (undefined methods, used by Laravel facades); __invoke (calling an object like a function); __clone (customise a clone, for example deep-copy a nested object); __serialize/__unserialize. They are powerful but hide behaviour from IDEs and static analysis, so use them deliberately.

What they are really testing: Breadth, and awareness of the cost to readability and tooling.

JuniorWhat is constructor property promotion?

PHP 8.0 lets a constructor declare and assign properties in one go: public function __construct(private string $name, private int $age = 0) {} declares $name and $age as private properties and assigns the arguments. It removes the boilerplate of declaring each property and writing $this->name = $name. Combined with readonly (8.1) and readonly classes (8.2), it makes small immutable value objects a few lines long.

What they are really testing: Fluency with PHP 8 syntax.

JuniorWhat are namespaces and why does PHP need them?

A namespace groups classes, functions and constants under a prefix, so App\Models\User and Vendor\Auth\User can coexist. use imports a name into a file, optionally with an alias. Namespaces map onto folders under PSR-4, which is how Composer finds a class file from its name. Before namespaces, libraries used long prefixed class names like Zend_Db_Table to avoid collisions.

What they are really testing: The link between namespaces, PSR-4 and autoloading.

JuniorWhat are enums in PHP?

PHP 8.1 added enums: a closed set of named cases. A pure enum (enum Suit { case Hearts; case Spades; }) has no values; a backed enum maps each case to an int or string (enum Status: string { case Paid = 'paid'; }) and gets from() (throws on an unknown value) and tryFrom() (returns null). Enums can have methods, constants and implement interfaces, and they work with match. They replace class constants and magic strings for statuses and types, and the type checker rejects an invalid value.

What they are really testing: from() versus tryFrom(), and using enums instead of string constants.

04

Junior: arrays, errors and security basics (7 questions)

These separate someone who has shipped PHP from someone who has read about it: arrays are everywhere, and SQL injection and XSS questions appear in almost every PHP screen. Background: Module 04, Module 08 and Module 10.

JuniorWhat is a PHP array, really?

An ordered hash map: keys (ints or strings) mapped to values, remembering insertion order. The same type serves as a list, a dictionary and a set. Lists with keys 0..n-1 are stored compactly ("packed"). Consequences: key lookup is O(1) but in_array is O(n); unset leaves gaps (array_values renumbers, array_is_list checks); numeric-string keys like "8" become ints. Arrays are values, so assigning one copies it (lazily, copy-on-write).

What they are really testing: Whether they know the cost model and the value semantics.

JuniorWhat is the difference between array_merge and the + operator on arrays?

array_merge($a, $b) appends and renumbers integer keys; for string keys, later values overwrite earlier ones. $a + $b is a union by key: for any key already in $a, the value from $b is ignored, and integer keys are not renumbered. So [1, 2] + [3, 4, 5] is [1, 2, 5], not five elements. Use + for "defaults filled in by missing keys" ($options + $defaults) and array_merge or the spread operator [...$a, ...$b] for lists.

What they are really testing: The [1,2] + [3,4,5] trap.

JuniorName the array functions you use most and what they do.

array_map (transform each element), array_filter (keep matching; it preserves keys, so wrap in array_values for a list), array_reduce (fold to one value), array_keys/array_values, array_column (pluck a field from rows, optionally keyed by another), array_key_exists, in_array, array_search, array_slice/array_splice, array_unique, array_flip, array_combine, usort/uasort/ksort, array_sum, implode/explode. PHP 8.4 added array_find, array_any and array_all.

What they are really testing: Working vocabulary, and the array_filter key-preservation detail.

JuniorWhat is the difference between errors and exceptions in PHP?

Historically, PHP reported problems as errors (notices, warnings, fatal errors) printed or logged by the engine, which try/catch could not catch. PHP 7 turned most fatal errors into Error objects (TypeError, ValueError, DivisionByZeroError, ArgumentCountError), so both Exception and Error implement Throwable. Warnings (such as a missing array key) are still not exceptions unless a handler converts them, which frameworks like Laravel do with set_error_handler. catch (Throwable $e) catches both families.

What they are really testing: The Throwable hierarchy and that warnings are not exceptions by default.

JuniorHow do try, catch and finally work?

Code that may fail goes in try. The first catch whose type matches handles the exception; one catch can list several types (catch (InvalidArgumentException | DomainException $e)), and since PHP 8.0 the variable is optional. finally runs whether or not an exception was thrown, even after a return, so it is the place to release resources. Rethrow with context using throw new OrderFailed("...", previous: $e) so the original stack trace is kept.

What they are really testing: Multi-catch, finally semantics and exception chaining with previous.

JuniorHow do you prevent SQL injection in PHP?

Never build SQL by concatenating user input. Use prepared statements with bound parameters through PDO: $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?'); $stmt->execute([$email]);. The query and the data travel separately, so the data can never be executed as SQL. Set PDO::ATTR_EMULATE_PREPARES to false where the driver supports native prepares, and PDO::ERRMODE_EXCEPTION. Identifiers (table and column names) cannot be bound, so validate them against an allow-list. ORMs like Eloquent and Doctrine bind parameters for you, except in raw expressions.

What they are really testing: Prepared statements, and the allow-list for identifiers that cannot be bound.

JuniorHow do you prevent XSS in PHP output?

Escape on output, for the context you are writing into. For HTML body and attribute values, htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'). Template engines do this by default: Blade's {{ $x }} and Twig's {{ x }} escape, while {!! $x !!} and |raw do not. URLs need urlencode plus a scheme check (reject javascript:), and data inside JavaScript should go through json_encode with the JSON_HEX_* flags. A Content-Security-Policy header is the second line of defence.

What they are really testing: Context-aware output escaping, not "sanitise the input".

05

Mid-level: memory, generators, closures and types (8 questions)

For roles with two to five years of experience: how the language works inside, because that is what lets you predict its failure modes.

Mid-levelHow does PHP manage memory?

Every value (a zval) is reference-counted. Arrays and strings use copy-on-write: assigning one shares it, and it is copied only when one side writes. When a refcount hits zero the memory is freed immediately. Objects that reference each other in a cycle never reach zero, so a cycle collector runs when its root buffer fills (gc_collect_cycles() forces it). Because each request ends and releases everything, leaks rarely matter in FPM, but they do in long-running workers (queue consumers, Swoole, RoadRunner, FrankenPHP worker mode), where you watch memory_get_usage() and restart workers after N jobs.

What they are really testing: Refcounting, copy-on-write, cycles, and why long-running PHP changes the picture.

Mid-levelWhat are generators and when would you use one?

A function containing yield returns a Generator: an iterator that runs the body lazily, pausing at each yield. It lets you stream data with constant memory, for example reading a 5 GB CSV line by line with fgetcsv inside a loop that yields each row, or paging through an API. yield $key => $value yields keys, yield from delegates to another iterable, and $gen->send() passes values back in. Laravel's lazy() and cursor() are generators over database results.

What they are really testing: Memory-bounded streaming as the main use case.

Mid-levelWhat are closures, and what does use do?

An anonymous function is a Closure object. It does not see outer variables automatically: use ($x) captures $x by value at creation time, and use (&$x) by reference. Arrow functions (fn($a) => $a * $rate) capture outer variables by value automatically but can only contain one expression. A closure created inside a class method binds $this; static function prevents that (useful to avoid keeping large objects alive). Closure::fromCallable and first-class callable syntax strlen(...) turn functions into closures.

What they are really testing: By-value capture timing, arrow-function capture and $this binding.

Mid-levelWhat does the match expression do differently from switch?

match (PHP 8.0) is an expression that returns a value, compares with strict ===, has no fall-through (each arm is one expression; several conditions can share an arm with commas), and throws UnhandledMatchError if nothing matches and there is no default. switch compares loosely with ==, falls through without break, and silently does nothing when unmatched. match (true) { $age < 13 => 'child', ... } handles ranges.

What they are really testing: Strictness, exhaustiveness and fall-through.

Mid-levelExplain PHP's type system features added in PHP 8.x.

Union types (int|string), mixed, static return type and named arguments (8.0); pure intersection types, never, enums and readonly properties (8.1); readonly classes, DNF types like (A&B)|null, and true/false/null as standalone types (8.2); typed class constants and #[\Override] (8.3); property hooks and asymmetric visibility (8.4). Types are checked at run time, and static analysers (PHPStan, Psalm) add generics through docblocks such as @return list<User> and @template T.

What they are really testing: Currency with the language, and knowing generics exist only in docblocks.

Mid-levelWhat are named arguments and what are their risks?

PHP 8.0 lets you pass arguments by parameter name: str_pad(string: $s, length: 10, pad_type: STR_PAD_LEFT). You can skip optional parameters and the call documents itself. The risk: parameter names become part of the public API, so renaming a parameter in a library is now a breaking change for callers who used the name. Named and positional arguments can be mixed, positional first. Spreading a string-keyed array (f(...$options)) passes named arguments.

What they are really testing: That they know renaming a parameter becomes a breaking change.

Mid-levelWhat are attributes in PHP?

Attributes (PHP 8.0) are structured metadata attached to classes, methods, properties, parameters and constants: #[Route('/users', methods: ['GET'])]. Each attribute is a class marked #[Attribute], and code reads them with Reflection ($method->getAttributes(Route::class)) and instantiates them with newInstance(). They replaced docblock annotations in Symfony routing, Doctrine mapping and PHPUnit (#[Test], #[DataProvider]). Built-ins include #[\SensitiveParameter] (hides a value in stack traces), #[\Override] and #[\Deprecated].

What they are really testing: That attributes are read through Reflection, and one real framework use.

Mid-levelHow do sessions and cookies work in PHP, and how do you secure them?

session_start() reads a session ID from a cookie (default name PHPSESSID), loads that session's data into $_SESSION from storage (files by default, Redis in production), and writes it back at the end of the request. Only the ID lives in the browser. Secure it with cookie flags HttpOnly (no JavaScript access), Secure (HTTPS only) and SameSite=Lax or Strict (CSRF defence); call session_regenerate_id(true) after login to prevent session fixation; enable session.use_strict_mode. File-based sessions lock per request, so long requests serialise parallel AJAX calls from the same user; call session_write_close() early when you only read.

What they are really testing: Cookie flags, session fixation and session locking.

06

Mid-level: design, frameworks and testing (9 questions)

Almost every PHP job is a Laravel or Symfony job, so mid-level rounds test how you structure code inside a framework and how you test it. Background: Module 12.

Mid-levelWhat is dependency injection and what is a service container?

Dependency injection means a class receives its collaborators (usually through the constructor) instead of creating them: __construct(private Mailer $mailer), not new SmtpMailer() inside. It makes classes testable (inject a fake) and swappable. A container builds the object graph for you: it reads constructor type hints with Reflection and resolves them recursively ("autowiring"), using bindings you register for interfaces ($app->bind(Mailer::class, SmtpMailer::class)). Laravel's service container and Symfony's DependencyInjection component are the two you meet. Pulling services out of the container inside business code ("service locator") hides dependencies and is considered an anti-pattern.

What they are really testing: DI versus a service locator, and how autowiring works.

Mid-levelExplain the SOLID principles with PHP examples.

Single responsibility: an InvoiceCalculator does not also send emails. Open/closed: add a new ShippingRule implementation instead of editing a growing switch. Liskov substitution: a subclass must honour the parent's contract; a ReadOnlyRepository that throws on save() breaks it. Interface segregation: small interfaces (Readable, Writable) instead of one 20-method interface. Dependency inversion: controllers depend on a PaymentGateway interface; the container binds Stripe in production and a fake in tests.

What they are really testing: Concrete PHP examples, and whether they can spot a violation.

Mid-levelWhat are PSR standards? Which ones matter most?

PHP Standards Recommendations from PHP-FIG, which let frameworks and libraries interoperate. PSR-4 autoloading (namespace-to-folder mapping); PSR-12 and its successor PER Coding Style (formatting); PSR-3 logger interface (Monolog implements it); PSR-7 HTTP messages and PSR-15 middleware/handlers; PSR-11 container interface; PSR-6/PSR-16 caching; PSR-14 events; PSR-18 HTTP client. Depending on the PSR interface instead of a concrete library lets you swap implementations.

What they are really testing: Knowing PSR-4 and PSR-12 at least, and why interfaces matter for interoperability.

Mid-levelWhat is the N+1 query problem and how do you fix it in Laravel or Doctrine?

Loading a list of N records and then lazily loading a relation for each one runs 1 + N queries: foreach (Post::all() as $post) echo $post->author->name;. Fix it with eager loading: Post::with('author')->get() runs two queries (posts, then all authors with WHERE id IN (...)). In Doctrine, a fetch join in DQL (JOIN p.author a with SELECT p, a). Detect it with Laravel Debugbar/Telescope, Model::preventLazyLoading() in development, or the Symfony profiler. For large exports, select only the columns needed and stream with cursor() or lazy().

What they are really testing: Recognising it, the eager-loading fix, and how to detect it.

Mid-levelActive Record versus Data Mapper: what is the difference?

In Active Record (Laravel Eloquent) a model object both holds the data and knows how to save itself: $user->save(). It is fast to write and great for CRUD, but domain objects are tied to the database schema and are harder to unit-test. In Data Mapper (Doctrine ORM) entities are plain objects and a separate EntityManager persists them, tracking changes in a unit of work and flushing them together. It suits complex domains and keeps entities framework-free, at the cost of more concepts.

What they are really testing: Trade-offs rather than a verdict.

Mid-levelHow do you write testable PHP code, and what do you test?

Inject dependencies through constructors, keep side effects (database, HTTP, clock, filesystem) behind interfaces, and keep business rules in plain classes that need no framework to run. Then: unit tests with PHPUnit or Pest for the rules, using fakes or mocks at the boundaries; integration tests against a real database (SQLite in memory, or Postgres/MySQL in Docker, with transactions rolled back per test); feature/HTTP tests through the framework ($this->postJson(...)). Inject a clock (Psr\Clock\ClockInterface) rather than calling time(). Data providers cover many inputs with one test method.

What they are really testing: Design for testability, not just knowing PHPUnit syntax.

Mid-levelWhat is Composer's lock file for, and install versus update?

composer.json states version constraints (^3.2). composer update resolves the newest versions that satisfy them and writes the exact versions (and hashes) to composer.lock. composer install installs exactly what the lock file says, so every developer, CI run and server gets identical dependencies. Commit the lock file for applications; libraries usually do not. Deploy with composer install --no-dev --optimize-autoloader, and run composer audit for known vulnerabilities.

What they are really testing: Reproducible builds and production install flags.

Mid-levelHow do you handle configuration and secrets in a PHP application?

Configuration that differs per environment comes from environment variables (twelve-factor), loaded from a .env file in development by vlucas/phpdotenv or symfony/dotenv. The .env with real secrets is never committed; commit a .env.example. In production, inject secrets from the platform (Kubernetes secrets, AWS Secrets Manager, Vault). Laravel caches config with php artisan config:cache, after which env() calls outside config files return null, which is a classic production bug. Mark sensitive parameters with #[\SensitiveParameter] so they never appear in stack traces.

What they are really testing: Env vars, not committed secrets, and the config-cache gotcha.

Mid-levelWhat is the difference between static properties and instance properties, and why be careful with static state?

An instance property belongs to each object; a static property belongs to the class and is shared by all instances. In classic PHP-FPM, static state resets on every request, so it seems harmless. In long-running runtimes (queue workers, Octane, RoadRunner, Swoole, FrankenPHP worker mode) it survives between requests, so a static cache or "current user" leaks data from one request or tenant into the next and grows memory without bound. Prefer state held in services with a clear lifetime (request-scoped) and reset them between requests.

What they are really testing: Awareness of long-running PHP and state leaking between requests.

07

Mid-level: runtime, performance and security (8 questions)

How PHP runs in production and how it gets attacked. Background: Module 10 and Module 11.

Mid-levelWhat does OPcache do, and what is the JIT?

OPcache stores the compiled opcodes of each script in shared memory, so PHP skips parsing and compiling on every request, often the biggest single speed-up available. In production set opcache.validate_timestamps=0 (never re-check files) and reset the cache on deploy, and consider preloading framework classes at server start. The JIT (PHP 8.0+) compiles hot opcodes to machine code; it helps CPU-bound work such as image processing or maths, but gives little to typical web requests, which spend their time waiting on the database and network.

What they are really testing: That OPcache matters far more than the JIT for web apps.

Mid-levelHow do you store passwords securely in PHP?

Never store the password, and never use md5 or sha1. Use password_hash($password, PASSWORD_DEFAULT), which currently produces a bcrypt hash with a random salt embedded in the result (PASSWORD_ARGON2ID is also available). Check with password_verify($input, $hash), which compares in constant time. After a successful login, call password_needs_rehash and update the stored hash if the algorithm or cost has changed. Rate-limit login attempts, and compare other secrets (tokens) with hash_equals.

What they are really testing: The built-in API, and rehashing and constant-time comparison.

Mid-levelWhat is CSRF, and how do PHP frameworks prevent it?

Cross-site request forgery: a malicious page makes the victim's browser submit a request to your site, and the browser attaches the victim's session cookie. Defences: a per-session CSRF token embedded in every form and checked on every state-changing request (Laravel's @csrf and VerifyCsrfToken middleware, Symfony forms), SameSite=Lax session cookies, and never changing state on GET. JSON APIs using bearer tokens in headers are not exposed the same way, because browsers do not attach those automatically.

What they are really testing: The mechanism, plus the SameSite and GET-is-safe defences.

Mid-levelWhat are the risks of unserialize() and file uploads?

unserialize() on user input can instantiate arbitrary classes and trigger their magic methods (__wakeup, __destruct), which "gadget chains" in common libraries turn into remote code execution. Use json_decode for untrusted data, or pass ['allowed_classes' => false]. For uploads: check $_FILES[...]['error'], enforce size limits, detect the real type with finfo rather than trusting the extension or the client MIME type, generate your own file name, store outside the web root (or in object storage), and never let an uploaded file be executed as PHP.

What they are really testing: Object injection and a safe upload checklist.

Mid-levelHow do you make a PHP HTTP API return consistent errors?

Throw domain exceptions from the code (OrderNotFound, ValidationFailed) and translate them in one place, the framework's exception handler or a PSR-15 middleware, into a consistent JSON body with the right status: 404, 422 with field errors, 409 for conflicts, 500 with a generic message and a correlation ID (never the stack trace). RFC 9457 application/problem+json is a good standard shape. Log 5xx errors with context; do not log 4xx noise at error level.

What they are really testing: Centralised exception mapping and not leaking internals.

Mid-levelHow would you process a slow task (sending emails, generating PDFs) without making the user wait?

Push it onto a queue and return immediately. The web request stores a job (Laravel Queues, Symfony Messenger) in Redis, a database table, RabbitMQ or SQS; separate long-running worker processes (php artisan queue:work, messenger:consume) pick jobs up, run them, and retry failures with back-off, moving permanent failures to a failed-jobs table. Jobs must be idempotent, since a job can run twice, and should receive IDs rather than whole models. Supervise workers with Supervisor or systemd, and restart them on deploy and after N jobs to avoid memory growth.

What they are really testing: Queues, workers, retries and idempotency.

Mid-levelHow do you debug a slow PHP page?

Measure before guessing. Turn on the framework profiler (Laravel Debugbar or Telescope, Symfony profiler) to see query count and time: N+1 queries and missing indexes are the usual culprits (EXPLAIN the slow query). Profile CPU and memory with Xdebug's profiler, Blackfire, Tideways or SPX to find the hot function. Check OPcache is enabled, that Composer's autoloader is optimised, and that nothing calls an external API synchronously in the request. In production, an APM (New Relic, Datadog, Sentry performance) shows which endpoints and spans are slow for real users.

What they are really testing: A measurement-first process with named tools.

Mid-levelWhat is the difference between PHP-FPM, Apache mod_php, and newer runtimes like FrankenPHP or RoadRunner?

mod_php embeds PHP in every Apache process, so even static file requests carry PHP's memory. PHP-FPM runs a separate pool of PHP workers behind Nginx or Apache over FastCGI, tuned with pm.max_children (roughly available memory divided by memory per worker). Both boot the application on every request. RoadRunner, Swoole and FrankenPHP in worker mode (and Laravel Octane on top of them) keep the application booted in memory and feed it many requests, which cuts latency sharply but brings long-running concerns: static state, memory leaks and database connections that must be reset between requests.

What they are really testing: The FPM process model and the trade-offs of persistent workers.

08

Senior: architecture, scaling and production (10 questions)

Senior PHP interviews assume the ecosystem around the language: a framework, a relational database, Redis, queues and a system running in production, often with a legacy codebase next to it. There is no single right answer; the model answers show the shape of a strong one: what you would ask first, what you would measure, and which trade-off you would accept.

SeniorHow would you structure a large Laravel or Symfony codebase so it stays maintainable?

Organise by domain (Billing, Catalog, Accounts), not only by technical layer, so a feature lives in one folder. Keep controllers thin: validate input (form requests or DTOs), call one application service or action class, return a response. Business rules live in plain PHP classes with no framework dependency; the ORM stays at the edges (repositories or query objects where Eloquent logic grows). Enforce boundaries with Deptrac or PHPStan rules, run PHPStan at a high level in CI, and use events or a message bus between domains instead of direct calls into each other's internals. Split into separate services only when there is an organisational or scaling reason.

What they are really testing: Modularity inside a monolith, enforced by tooling, and not reaching for microservices by default.

SeniorHow do you scale a PHP application under growing traffic?

PHP's shared-nothing model scales horizontally well: put stateless app servers behind a load balancer and move all state out (sessions and cache to Redis, uploads to object storage). Then work down the bottlenecks you measure: OPcache and preloading; HTTP and CDN caching; application caching with Redis (cache-aside, with sensible TTLs and stampede protection); database read replicas, indexes and query fixes; queues for anything slow; connection pooling (PgBouncer, ProxySQL). Tune FPM pm.max_children from memory per worker. Consider Octane/RoadRunner/FrankenPHP for latency-sensitive endpoints once the app is safe for long-running workers.

What they are really testing: Measure-first scaling, stateless servers and knowing where PHP spends its time.

SeniorHow would you upgrade a legacy PHP 5 or 7 application to PHP 8.x?

First get a safety net: characterisation tests on the HTTP surface (even snapshot tests of key pages) and error logging with all deprecations enabled. Run Rector with PHP-version sets to automate syntax upgrades, and PHPStan/PHPCompatibility to find what will break: removed functions (mysql_*, each, create_function), string-to-number comparison changes in 8.0, stricter internal function argument errors, dynamic property deprecation in 8.2, null passed to non-nullable internal parameters in 8.1. Upgrade Composer dependencies in step. Move one minor version at a time, deploy each behind canaries, and add strict_types and types gradually after the upgrade, not during it.

What they are really testing: An incremental, tool-assisted plan and concrete knowledge of what changed.

SeniorDesign an idempotent payment webhook handler in PHP.

Verify the signature first (hash_hmac over the raw body, compared with hash_equals), and reject stale timestamps. Store the provider's event ID in a table with a unique constraint; inserting it inside the same database transaction as the state change makes a duplicate delivery fail harmlessly. Return 2xx quickly and push the real work to a queue, because providers retry on timeouts. Make the state transition itself idempotent ("mark order paid if pending"), handle events arriving out of order by checking the current state, and log the event ID for tracing. Reconcile periodically against the provider's API to catch missed webhooks.

What they are really testing: Signature checks, unique constraints for deduplication and at-least-once delivery.

SeniorHow do you handle database migrations with zero downtime?

Every migration must be compatible with both the old and the new code, because both run during a rolling deploy. Use expand and contract: add a nullable column (expand), deploy code that writes both old and new, backfill in batches through a queued job, deploy code that reads the new one, then drop the old column in a later release (contract). Avoid long locks: create indexes concurrently in Postgres (or use online DDL tools such as gh-ost or pt-online-schema-change for MySQL), never rename a column in one step, and never run a migration that rewrites a large table during peak traffic.

What they are really testing: Expand-contract and awareness of table locks.

SeniorHow do you make a PHP service observable in production?

Structured JSON logs through Monolog (a PSR-3 logger) with a request or correlation ID on every line, and context arrays instead of interpolated strings. Metrics (request rate, error rate, latency percentiles, queue depth, FPM active workers) exported to Prometheus or an APM. Distributed tracing with OpenTelemetry for PHP, so a slow request shows which query or HTTP call took the time. Error tracking (Sentry) with releases tagged. Health endpoints that check the database and Redis, used by the load balancer. Alert on symptoms users feel (error rate, p95 latency), not on every log line.

What they are really testing: Logs, metrics and traces, correlated, with alerting on symptoms.

SeniorHow would you design caching for a read-heavy PHP API?

Layer it. HTTP caching (Cache-Control, ETags) and a CDN for public responses. Application caching in Redis with cache-aside (Cache::remember), keys that include every input that changes the result (including the tenant or locale), TTLs plus explicit invalidation on writes (or versioned keys). Protect against stampedes when a hot key expires: a lock so one process rebuilds while others serve the stale value, or probabilistic early refresh. Never cache per-user data under a shared key. Measure hit rate, and remember the cache is an optimisation: the system must be correct if Redis is empty.

What they are really testing: Invalidation strategy, stampede protection and multi-tenant key safety.

SeniorHow do you secure a multi-tenant PHP SaaS application?

Tenant isolation first: every query must be scoped to the current tenant, enforced centrally (a global scope in Eloquent, a Doctrine filter, or Postgres row-level security) rather than remembered in each query, and covered by tests that try to read another tenant's data. Authorisation through policies or voters, never only by hiding UI. Cache keys, queue jobs, file paths and search indexes must all carry the tenant ID. Standard web security on top: prepared statements, output escaping, CSRF, strict session cookies, rate limiting, dependency auditing (composer audit), secrets out of the repo, and audit logs for admin actions.

What they are really testing: Centralised tenant scoping, and remembering caches, queues and files.

SeniorWhen would you choose PHP for a new service, and when not?

PHP with Laravel or Symfony is an excellent choice for web applications, APIs, admin panels, e-commerce and content platforms: fast development, a huge ecosystem, cheap hosting, easy horizontal scaling and a large hiring pool. It is a weaker fit for CPU-heavy work (video processing, machine learning), long-lived connections at very high concurrency (though Swoole, ReactPHP and FrankenPHP narrow the gap), and low-latency systems needing fine control over memory. A senior answer weighs the team's skills and the existing platform more than benchmarks.

What they are really testing: Pragmatism: team and ecosystem over language fashion.

SeniorWalk through what you check when reviewing a PHP pull request.

Correctness against the requirement, including edge cases and error paths. Security: input validation, parameter binding, escaping, authorisation checks on every new endpoint, secrets. Data: migrations are backward-compatible, queries are indexed and not N+1, transactions wrap multi-step writes. Tests cover the new behaviour and would fail without the change. Design: the change sits in the right layer, and new abstractions earn their keep. Static analysis and code style pass in CI (PHPStan, PHP-CS-Fixer). Operations: logging, feature flags for risky changes, and a rollback plan. Comments are specific, kind and separate blocking issues from suggestions.

What they are really testing: A structured review that covers security, data and operations, not just style.

The senior-answer shape
Clarify the goal and constraints, name two options with their costs, pick one and say what would make you change your mind, then say how you would verify it in production. That structure matters more than any single fact.
09

The coding round, walked through

A live coding round is 30 to 45 minutes on one or two problems in a shared editor. The interviewer grades how you think, communicate and test, not only whether it runs. Follow the same script every time, the one from Module 14.

  1. 1
    Clarify (2 min)

    Restate the problem. Ask about input size, empty input, duplicates, types (could an ID arrive as a string?), ordering of the output, and what to return when there is no answer. Write the contract as a comment.

  2. 2
    Example (1 min)

    Work one small case by hand. It becomes your first test.

  3. 3
    Brute force out loud (2 min)

    "For every request, count the same client's requests in the next window: O(n²)." Say it and its cost before improving it.

  4. 4
    Pick the pattern (1 min)

    Group by key in an array? Sort then slide a window? A heap? Name it, and why.

  5. 5
    Code (15 min)

    Talk while you type. Real names, type declarations, ===, and the edge cases you listed.

  6. 6
    Test (5 min)

    Run your example, then the edges. Finding your own bug scores higher than never having one.

  7. 7
    Complexity and improvements (2 min)

    State time and space, then the better version if there is one.

A typical 30-minute problem solved that way: given a log of [client, second] requests in no particular order, return every client that made more than $limit requests within any $window-second span, sorted by name. The version below groups by client with an array, sorts each client's times, and slides a window over them (Module 14, pattern 2).

phpmain.php
<?php
declare(strict_types=1);

// Contract: $log is unsorted and may be empty; $limit >= 1; $window >= 1 second.
// A client offends if more than $limit requests fall within $window seconds
// (times t and t + window - 1 are in the same span). Result sorted by name.
function offenders(array $log, int $limit, int $window): array
{
    $byClient = [];
    foreach ($log as [$client, $second]) {
        $byClient[$client][] = $second;
    }

    $result = [];
    foreach ($byClient as $client => $times) {
        sort($times);                                  // O(k log k) per client
        $left = 0;
        foreach ($times as $right => $t) {
            while ($t - $times[$left] >= $window) $left++;
            if ($right - $left + 1 > $limit) {
                $result[] = (string) $client;          // numeric names become int keys
                break;
            }
        }
    }
    sort($result, SORT_STRING);
    return $result;
}

$log = [
    ['ana', 1], ['bo', 2], ['ana', 3], ['ana', 5], ['bo', 30], ['cy', 7],
    ['ana', 70], ['bo', 31], ['bo', 33], ['cy', 50], ['bo', 32],
];
echo implode(', ', offenders($log, limit: 2, window: 10)), "\n";
echo implode(', ', offenders($log, limit: 3, window: 10)), "\n";
echo count(offenders([], limit: 1, window: 1)), "\n"; // empty log
Outputcompiled & run with real PHP
ana, bo
bo
0

ana makes 3 requests in seconds 1 to 5, so she offends at a limit of 2 but not 3; bo makes 4 in seconds 30 to 33. Overall O(n log n) for the sorts, O(n) for the windows, O(n) extra memory.

Your turn

The interviewer follows up: "the log is now an endless stream, already in time order". Rewrite it to keep an SplQueue of recent times per client, dropping times that fall out of the window as each request arrives. What is the memory cost now?

What loses the round

  • Silence for ten minutes, then a wall of code
  • Using == and in_array without the strict flag
  • Forgetting that a client named "123" becomes an int array key
  • "It should work" without running an example
  • Optimising before the brute force is correct

What wins it

  • Narrating your reasoning, including dead ends
  • A written contract and example before code
  • Testing edge cases: empty log, a limit no one exceeds, requests exactly $window seconds apart
  • Naming the complexity without being asked
  • "I sort each client's times so the window only ever moves forward"
10

Take-home assignment checklist

PHP take-homes are usually "build a small API or app in Laravel/Symfony" or "process this file and report on it". Reviewers open the README, run the install and the tests, read the tests, then the code. Most rejected submissions fail at step two: it does not run on the reviewer's machine.

  • It runs on a clean machine: composer install and the test command work with only PHP and Composer installed, or with one docker compose up. State the PHP version in composer.json ("php": "^8.3") and commit composer.lock.
  • README: what it does, how to install, run and test it in three commands, example requests (curl lines), and the decisions you made, including what you deliberately left out.
  • Tests with PHPUnit or Pest: the happy path, empty and invalid input, and the one tricky rule in the spec. Feature tests through the framework for an API, with an in-memory SQLite or a Docker database.
  • Static analysis and style: PHPStan at a sensible level passing, and code formatted with PHP-CS-Fixer or Laravel Pint. Reviewers notice.
  • Structure: thin controllers, validation in form requests or DTOs, business logic in plain classes that can be tested without the framework, dependencies injected through constructors.
  • Modern PHP: declare(strict_types=1), typed properties and return types, readonly value objects, enums for statuses, match instead of long switches.
  • Errors on purpose: invalid input returns 422 with field errors, missing records 404, not a 500 with a stack trace.
  • No noise in the repo: no vendor/, no .env with secrets (commit .env.example), no IDE folders, no commented-out code.
  • Time-box to what they asked (usually 3 to 4 hours) and say so in the README. Five half-done extras are a red flag; one finished extra is fine. A handful of meaningful commits beats one "final" dump.
The sentence reviewers want to write
"Ran first time, tests cover the edge cases, the code reads like the team already wrote it." Aim every decision at that sentence. The next module, Job Ready, turns the same standards into a portfolio.

Frequently asked questions

What PHP topics are asked most in interviews?
At junior level: == versus ===, types and strict_types, isset versus empty, arrays and their functions, OOP (interfaces, abstract classes, traits), exceptions, and SQL injection and XSS. At mid level: memory and copy-on-write, generators, closures, dependency injection, SOLID, N+1 queries, sessions, OPcache and password hashing. Senior rounds add architecture, scaling, upgrades, zero-downtime migrations and production observability.
Do PHP interviews expect Laravel or Symfony knowledge?
For most jobs, yes. Junior screens focus on the language, but mid and senior rounds usually assume a framework: the service container, Eloquent or Doctrine, queues, middleware and testing. Read the posting: it almost always names the framework, and the interview follows it.
Which PHP version should I prepare for?
Prepare on PHP 8.3 or later and know which features arrived when: union types, match, named arguments and constructor promotion (8.0); enums, readonly properties and fibers (8.1); readonly classes (8.2); typed class constants (8.3); property hooks and asymmetric visibility (8.4). Many teams still maintain PHP 7.4 code, so know what changed in 8.0.

Finish the PHP handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.