PHP
10 PHP Backend Interview Questions and Answers
Practice 10 PHP backend interview questions with answers and code examples covering PHP-FPM, strict types, Composer, SQL, queues, and application security.
These PHP backend interview questions cover language behavior, application architecture, databases, background jobs, and security. They draw on DeepRound's prepared PHP interview questions.
Predict each code example before reading the answer, then explain the mechanism. For design questions, walk through a failure or retry. The examples use PHP 8 syntax; SQL examples specify PostgreSQL where its behavior matters.
1. When should you use array_key_exists() instead of isset()?
Use array_key_exists() to distinguish a missing key from a key whose value is null. isset() returns false in both cases.
$data = ['name' => null];
var_dump(isset($data['name'])); // bool(false)
var_dump(array_key_exists('name', $data)); // bool(true)
var_dump(array_key_exists('email', $data)); // bool(false)For a partial profile update, an omitted name might mean "leave unchanged," while null means "clear it." Check key presence, then validate the value. Using ?? also loses this distinction because it falls back for both missing and null values. See the PHP key-presence documentation.
2. What does declare(strict_types=1) change?
Strict typing rejects incompatible scalar arguments instead of applying the usual coercion. Consider each call separately:
declare(strict_types=1);
function loadUser(int $id): void
{
// Load the user.
}
loadUser("42"); // TypeError
loadUser($_GET["id"]); // TypeError when the query contains id=42Both arguments are strings. For these user-defined calls, the calling file controls strictness; an integer is still accepted for a float parameter. See PHP's strict typing rules.
At the HTTP boundary, validate that the ID is present, represents an integer, and falls within the allowed range. Then pass the resulting integer to the service. Casting arbitrary input can silently turn invalid data into an apparently valid value.
3. Why do arrays and objects behave differently as arguments?
Predict the caller's values:
final class User
{
public function __construct(public string $name) {}
}
function update(array $tags, User $user): void
{
$tags[] = "php";
$user->name = "Updated";
}
$tags = ["backend"];
$user = new User("Alex");
update($tags, $user);$tags remains ["backend"]; $user->name becomes "Updated". Arrays passed without & have value semantics, with copy-on-write available internally. Passing an object copies its identifier, so both variables access the same object. The PHP objects documentation explains this distinction.
Reassigning the function's $user to a new object would not replace the caller's variable. Mutating an object and reassigning a variable are separate operations.
4. How does PHP-FPM handle an HTTP request?
The web server forwards the request through FastCGI. An available PHP-FPM worker initializes a PHP request, runs the application, and returns the response.
An ordinary worker executes one request at a time. Its process can survive across requests, while request shutdown clears ordinary userland objects and static property values. The pool's pm.max_children setting limits simultaneous requests; see the FPM configuration reference.
A queue job ending inside a continuing PHP process does not trigger that request shutdown. A static tenant ID can therefore survive into the next job. Pass tenant identity explicitly and clean up job-specific state. Laravel's queue worker documentation highlights this persistence.
5. What do Composer autoloading and OPcache each handle?
Composer locates class files. With a PSR-4 mapping from App\ to src/, the class App\Service\InvoiceService maps to src/Service/InvoiceService.php. Including vendor/autoload.php registers the generated loader, which PHP invokes when it needs an unloaded class. See Composer's PSR-4 mapping rules.
OPcache retains compiled PHP bytecode in shared memory, reducing repeated source loading, parsing, and compilation. It does not cache arbitrary function results, database queries, or API responses. The OPcache documentation describes its scope.
Explain which cost each mechanism addresses. Slow database calls remain slow even when class loading and compilation are optimized.
6. How would you import a large CSV without exhausting memory?
Read and process records incrementally. Assume $path identifies an existing file and processRow() validates and writes a row without retaining earlier rows:
$handle = fopen($path, 'rb');
if ($handle === false) {
throw new RuntimeException('Cannot open CSV');
}
try {
while (($row = fgetcsv($handle, escape: '')) !== false) {
processRow($row);
}
} finally {
fclose($handle);
}The explicit escape argument avoids the deprecated default and disables PHP's proprietary CSV escaping; see fgetcsv.
Bound downstream work too: flush small database batches, release ORM references, and limit retained errors. One enormous record can still consume substantial memory, so untrusted files also need enforceable record and field limits.
7. How would you support multiple external providers?
Suppose users choose an LLM model backed by different provider SDKs. Keep a server-owned catalog mapping each public model ID to its provider, provider-specific ID, and capabilities. Validate the choice before resolving the provider.
The application service calls an application-owned interface. Provider adapters translate requests and responses to their SDKs. Construct clients and adapters during bootstrap and inject them into a registry; the selection method chooses an already configured adapter. Models sharing one provider API can usually share an implementation.
Test the routing with fake adapters: select a model mapped to the second provider and verify that it receives the expected request without a live API call.
8. How would you paginate orders without a large OFFSET?
Large offsets require the database to compute skipped rows, and new inserts can shift page boundaries. PostgreSQL's pagination documentation explains this cost.
Use a cursor containing the last row's ordering values. Assume created_at is non-null and id is the primary key:
SELECT id, total_cents, created_at
FROM orders
WHERE user_id = :user_id
AND (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 50;The ID breaks timestamp ties. Add an index on (user_id, created_at DESC, id DESC) through a migration and verify the plan. The first page omits the cursor condition.
In PHP, validate the cursor, preserve timestamp precision, and bind values through PDO. Derive user scope from authenticated identity. This avoids offset shifts but does not freeze results when ordering values change.
9. How should a PHP API run and retry a background import?
Store the file durably, create a persistent import record, and return its ID and status location, typically with 202 Accepted. Queue a small reference to the import. The worker processes bounded batches and records completion or failure.
Protect the handoff: write an outbox entry with the import in one transaction, or let workers claim durable pending records directly. Use stable record identities and database constraints to make repeated processing safe.
Retry temporary storage failures with bounded delays and jitter. Reject permanently invalid input without repeatedly processing it. After retries are exhausted, persist a failed status and retain diagnostic context.
Symfony Messenger provides retry strategies, failure transports, and UnrecoverableMessageHandlingException for permanent failures. Its retry documentation explains the controls. Swallowing every exception can make failed work appear successful.
10. How do you prevent SQL injection with search and sort inputs?
Bind search values through prepared statements. Map sort options to trusted column names: PDO parameters cannot represent identifiers.
Assume $pdo throws exceptions on database errors:
$term = $_GET['q'] ?? '';
$sort = $_GET['sort'] ?? 'created_at';
$columns = ['email' => 'email', 'created_at' => 'created_at'];
if (!is_string($term) || !is_string($sort) || !isset($columns[$sort])) {
throw new InvalidArgumentException('Invalid search input');
}
$column = $columns[$sort];
$statement = $pdo->prepare(
"SELECT id, email FROM users
WHERE email LIKE :pattern
ORDER BY {$column} DESC
LIMIT 50"
);
$statement->execute(['pattern' => '%' . $term . '%']);
$rows = $statement->fetchAll();The column comes from the allowlist; the search term stays data. Map invalid input to a controlled client error. This example permits LIKE wildcards. If search must treat % and _ literally, escape their wildcard meaning separately.
Turn questions into interview practice
Answer aloud, then challenge one assumption: a worker restarts, storage fails, or two jobs process the same import. Use the backend preparation guide to plan your study and the trade-off framework to structure longer answers.
Take the PHP backend mock interview at junior, middle, or senior level, then review feedback by competency and answer.