Interview questions

PHP interview questions for senior developers (2026)

Practical PHP questions for PHP 8.4 and current Laravel and Symfony, from type juggling and references to queues, long-running workers and legacy upgrades.

These 26 PHP interview questions are for hiring developers who build and maintain real PHP applications: Laravel and Symfony products, APIs and the long-lived codebases many companies still depend on. They test modern PHP 8.4, the request lifecycle, framework internals, performance and the upgrade work that separates a senior PHP engineer from someone who has only started new projects. At Ryz, PHP candidates are sourced by recruiters who verify what they shipped, then take structured NTRVSTA AI interviews on topics like these, with recruiters reviewing them before and after. AI scores are advisory, and every decision is made by people.

How to use these questions

PHP spans a huge range, from WordPress plugins to large Symfony platforms, so start by deciding which PHP you are hiring for and pick questions to match. Then:

Fundamentals

What does 0 == "a" return in PHP 8, and why does it matter?

It returns false. Before PHP 8, comparing a number with a non-numeric string cast the string to a number, so it was true. PHP 8 compares them as strings instead. Loose comparison still has surprises, such as "1e3" == "1000" being true, which has caused authentication bugs when comparing hashes. Use === by default and hash_equals for secrets.

What a strong answer shows: They know type juggling changed in PHP 8 and still default to strict comparison.

What does declare(strict_types=1) actually do?

It makes scalar type declarations strict for function calls made from that file, so passing "5" to an int parameter throws a TypeError instead of coercing. For parameters, the calling file's mode decides; for return values, the mode of the file that declares the function decides. It does not change arithmetic or comparisons. Most modern codebases put it at the top of every file.

What a strong answer shows: The precise per-file, caller-side rule, which most candidates get wrong.

What does this print, and why?

$items = [1, 2, 3];
foreach ($items as &$item) {}
foreach ($items as $item) {}
print_r($items); // [1, 2, 2]

After the first loop, $item is still a reference to the last element. The second loop assigns each value into that reference, overwriting the last element on every pass. Call unset($item) after a by-reference loop, or avoid references and use array_map.

What a strong answer shows: They understand references, and that arrays are otherwise copy-on-write values.

Explain the PHP-FPM request lifecycle. What survives between requests?

PHP-FPM is shared-nothing: each request boots the app, handles the request and tears down all objects, statics and globals. What survives is the compiled bytecode in OPcache, persistent connections if enabled, and anything in external stores like Redis or the database. That model is why PHP apps scale horizontally easily and why in-memory state cannot be used for sessions or caches across requests.

What a strong answer shows: A runtime model that explains both PHP's strengths and its constraints.

How should passwords be stored in PHP?

With password_hash($plain, PASSWORD_DEFAULT) and password_verify, which handle salts and algorithm choice. Use password_needs_rehash on login to upgrade old hashes when cost or algorithm changes. Never MD5 or SHA-1, and never your own salt scheme.

What a strong answer shows: Standard library first, plus a migration path for existing hashes.

How do you prevent SQL injection when not using an ORM?

Use PDO prepared statements with bound parameters for every value. Identifiers like column names in an ORDER BY cannot be bound, so check them against an allowlist. Set PDO::ATTR_ERRMODE to ERRMODE_EXCEPTION, which is the default since PHP 8, and consider turning off emulated prepares for MySQL so the server handles parameters natively.

What a strong answer shows: They know where binding does not help.

How do enums and readonly classes improve a domain model?

Backed enums (PHP 8.1) replace string constants with a closed set of values that can carry methods. Readonly properties and classes (8.1 and 8.2) make value objects immutable without boilerplate.

enum OrderStatus: string {
case Pending = 'pending';
case Paid = 'paid';
case Refunded = 'refunded';

public function isFinal(): bool {
return match ($this) {
self::Paid, self::Refunded => true,
self::Pending => false,
};
}
}

$status = OrderStatus::tryFrom($row['status']) ?? throw new UnexpectedValueException();

What a strong answer shows: Comfort with match, from versus tryFrom, and typed value objects.

Intermediate

A script that imports a 2 GB CSV dies with "Allowed memory size exhausted". How do you fix it?

Do not load the file into an array. Stream it with a generator and process rows in batches, committing every few thousand rows.

function csvRows(string $path): Generator {
$handle = fopen($path, 'r') ?: throw new RuntimeException("Cannot open $path");
try {
while (($row = fgetcsv($handle, null, ',', '"', '')) !== false) {
yield $row;
}
} finally {
fclose($handle);
}
}

Also watch the ORM: Eloquent's lazy() or chunkById(), and clearing Doctrine's identity map with $em->clear(), keep memory flat.

What a strong answer shows: Streaming as the default for large data, and awareness that ORMs hold references.

What do OPcache, preloading and the JIT each do, and which matter for a web app?

OPcache stores compiled bytecode in shared memory so scripts are not recompiled per request; it is essential. Preloading loads chosen classes at server start for a small extra gain. The JIT compiles hot code to machine code and helps CPU-heavy work like image processing or math, but typical web requests spend their time on I/O, so the benefit is usually small.

What a strong answer shows: They know where time goes in a PHP request.

How has error handling changed in PHP 8?

Many internal functions now throw TypeError or ValueError instead of emitting warnings and returning false. Both Exception and Error implement Throwable, so a top-level handler should catch Throwable. Warnings and notices still exist and can be converted to exceptions with set_error_handler, which frameworks do for you.

What a strong answer shows: They know the exception hierarchy and do not rely on @ suppression.

You move a Laravel app from one server to three behind a load balancer and users get logged out randomly. Why?

Sessions are stored on local disk, so each server only knows its own sessions. Move sessions, cache and locks to Redis or the database, put uploaded files on shared object storage, and make sure every server has the same APP_KEY so encrypted cookies decrypt everywhere.

What a strong answer shows: They spot all the local state, not just sessions.

What changes when you run Laravel on Octane, or PHP on FrankenPHP or RoadRunner worker mode?

The app boots once and serves many requests from the same process, which removes bootstrap cost. The trade-off is that state now leaks between requests: static properties, singletons holding request data and growing arrays persist. Code must reset per-request state, and workers should restart after a set number of requests to contain leaks.

What a strong answer shows: They see that the shared-nothing guarantee is gone and know what to audit.

A cached report expires and the database spikes as hundreds of requests rebuild it. How do you stop that?

That is a cache stampede. Use a lock so one request rebuilds while others serve stale data or wait, for example Laravel's Cache::lock or Symfony Cache's built-in stampede protection. Add jitter to expiry times, and for expensive reports, refresh in a scheduled job rather than on read.

What a strong answer shows: They recognize the pattern and know several mitigations.

Modern PHP and Laravel/Symfony

What do PHP 8.4 property hooks and asymmetric visibility replace?

They remove most getter and setter boilerplate. Hooks run logic on read or write of a property, and asymmetric visibility lets a property be public to read but private to write.

final class Customer {
public string $email {
set(string $value) {
$this->email = strtolower(trim($value));
}
}

public private(set) DateTimeImmutable $createdAt;

public function __construct(string $email) {
$this->email = $email;
$this->createdAt = new DateTimeImmutable();
}
}

What a strong answer shows: Current PHP knowledge and judgment about when hidden logic in a property hook hurts readability.

A Laravel orders page runs 300 queries. How do you find and fix it?

Laravel Debugbar or Telescope shows the queries. The cause is usually lazy-loading relations in a loop or a Blade view. Eager load what the page needs and make lazy loading fail loudly outside production.

// AppServiceProvider::boot()
Model::preventLazyLoading(! app()->isProduction());

$orders = Order::with(['customer', 'lines.product'])
->withCount('refunds')
->latest()
->paginate(50);

What a strong answer shows: They fix the cause and add a guard so it does not come back.

How do you make Laravel queue jobs safe to retry?

Assume any job can run more than once. Make the work idempotent, for example by checking a processed flag or using a unique constraint. Set $tries, backoff and a failed() handler, use ShouldBeUnique or the WithoutOverlapping middleware to prevent duplicates, and dispatch after the database transaction commits. Horizon gives visibility into Redis queues.

What a strong answer shows: Idempotency first, then framework features.

How does Symfony's service container work, and why does it compile?

Services are defined in configuration and autowired by type. At build time the container is compiled to plain PHP, with unused private services removed, so runtime resolution is fast and many wiring errors appear at build time instead of in production. Compiler passes let bundles modify definitions, for example collecting tagged services.

What a strong answer shows: They understand the container as compiled code, not runtime reflection.

Where do attributes help in modern PHP, and where do they go too far?

Attributes (PHP 8.0) attach structured metadata that frameworks read through reflection: Symfony's #[Route] and #[AsCommand], Doctrine ORM mappings, validation constraints. The engine also understands a few itself, such as #[\Override] (8.3), which fails if the parent method disappears, and #[\Deprecated] (8.4). They go too far when business rules hide in attribute configuration that nobody can test or trace.

What a strong answer shows: They know attributes are inert metadata until something reads them, and use the engine-level ones as safety checks.

What belongs in a modern PHP project's tooling?

Composer with a committed composer.lock and PSR-4 autoloading, deployed with composer install --no-dev --optimize-autoloader. PHPStan or Psalm at a high level, with a baseline for legacy code. A formatter like PHP-CS-Fixer or Laravel Pint, Rector for automated refactors and upgrades, and Pest or PHPUnit in CI.

What a strong answer shows: Static analysis as standard practice.

A user sets themselves as admin by adding is_admin=1 to a profile form. What went wrong?

Mass assignment: the controller passed $request->all() to update() on a model without a restrictive $fillable. Pass only validated data, such as $request->validated() from a Form Request, and keep privileged fields out of fillable attributes. Authorization belongs in policies, not controllers.

What a strong answer shows: Security habits built into framework usage.

Senior and architecture

How would you upgrade a large PHP 7.4 application to PHP 8.4?

Add PHPStan with a baseline and enough tests to trust changes. Run Rector sets for each version to automate most edits, then deal with behavior changes: string-to-number comparisons, internal functions that now throw, dynamic properties deprecated in 8.2 and implicitly nullable parameters deprecated in 8.4. Upgrade the framework and dependencies in step, deploy one version jump at a time and watch deprecation logs.

What a strong answer shows: An incremental, tool-assisted plan with knowledge of what actually breaks.

A Laravel app on six servers hits "too many connections" on MySQL during peaks. Walk through the fix.

Do the math: six servers with pm.max_children = 100 can open 600 connections. Cap FPM workers to what the database can serve, move slow work to queues, add a proxy such as ProxySQL, send reads to replicas with Laravel's read and write connections, and fix the slow queries that hold connections. More workers often makes it worse.

What a strong answer shows: Capacity math across tiers rather than raising limits.

How would you structure a growing Laravel or Symfony codebase?

Group code by domain module rather than one huge app/Models and app/Http, with clear public entry points per module. Communicate between modules through events or service interfaces, keep Eloquent models from leaking across boundaries, and enforce rules with Deptrac or PHPStan rules. Extract a service only when a module has a real reason to deploy separately.

What a strong answer shows: Boundaries you can enforce, not just folders.

How do you ship database migrations without downtime?

Expand and contract. Add new columns or tables in a backward-compatible migration, deploy code that writes to both, backfill in batches, switch reads, then remove the old column in a later release. Avoid long table locks on large MySQL tables by using online DDL or tools like gh-ost, and make deploys atomic with a symlink switch and an OPcache reset.

What a strong answer shows: Releases where old and new code run at the same time without errors.

You are reviewing a PHP app for security. What do you look for first?

unserialize on user input, which enables object injection; file uploads stored in the web root or trusted by extension; SSRF in URL fetchers; missing CSRF protection; raw SQL with string concatenation; outdated packages, checked with composer audit; and secrets committed to the repo. Then authorization: can user A read user B's records by changing an ID?

What a strong answer shows: A prioritized checklist drawn from real PHP vulnerabilities.

How would you design webhook handling for a payments integration in PHP?

Verify the signature with hash_hmac and hash_equals against the raw body, reject old timestamps to block replays, store the event ID with a unique constraint and return 200 quickly. Process the event in a queued job that is idempotent, because providers retry and may deliver out of order.

What a strong answer shows: Verification, idempotency and fast acknowledgement as one design.

Red flags to watch for

A practical exercise

Provide a small Laravel 12 or Symfony 7 app with a MySQL or Postgres database in Docker. It has an endpoint that lists invoices and a command that imports payments from a large CSV. Plant issues: N+1 queries on the listing, the importer loading the whole file into memory, a mass assignment hole, and a queued job that double-charges when retried. Give the candidate a take-home of about three hours, or work through it together in 90 minutes.

Hire senior PHP developers vetted with these questions

Ryz introduces senior PHP developers who have been assessed on modern PHP, Laravel or Symfony and legacy upgrade work. They are the top 1% of the candidates we interview, they join your team, repos and standups, and they work within ±1h of US time zones. See how our vetting process runs, or use the PHP developer job description to scope the role.

FAQ

Should I hire for Laravel or Symfony experience specifically?

Hire for PHP depth first. A strong engineer moves between the two in weeks, since both share Composer, PSR standards and similar patterns. Framework experience matters more when the role starts with a large upgrade or deep framework customization.

How do I assess someone for a legacy PHP codebase?

Show them a messy, real-looking file and ask how they would make a safe change to it. Look for characterization tests, small refactors and static analysis, rather than an immediate rewrite.

Are WordPress developers a fit for application PHP roles?

Some are, especially those who build custom plugins with tests and Composer. Ask the same questions about types, queries, security and architecture rather than judging by the platform on their resume.

Questions we didn't answer? Email info@ryzlabs.com.

Explore Ryz Labs

Staff augmentationDedicated development teamsAI pod teamsForward deployed engineersNearshore software developmentAI engineering teamsHire engineers by roleRyz Labs vs competitorsAlternatives guidesBuyer guidesCase studiesHow we vet engineers
Ryz Labs

Senior engineers in your time zone. AI pod teams that ship.

Tell us who you need. You'll get a scoped plan, a price and the names of the people who would do the work.

Start a conversation →