15 selected PHP backend interview questions grouped by seniority level. Use them to review fundamentals, practical trade-offs, and senior-level production reasoning.
1How does PHP's type system distinguish weak and strict typing, and what changes when declare(strict_types=1) is enabled?
PHP normally permits compatible scalar coercions for declared parameter and return types, such as a numeric string passed to an int parameter. declare(strict_types=1) enables strict scalar checks: an incompatible argument or return value raises TypeError, although an int is still accepted for float. For user-defined function arguments, the calling file controls strictness; the defining file controls its return checks. Calls made from internal PHP functions have their own caveats and are not simply governed by the outer caller's strict_types setting. The directive must appear at the start of its PHP file. It does not make PHP statically typed or disable conversions in operators throughout the language.
// THREE SEPARATE FILES: save each PHP block under its indicated filename.
// File: lib.php
<?php
function takesInt(int $x): int { return $x; }
?>
// File: weak.php
<?php
require __DIR__ . '/lib.php';
echo takesInt('123'); // 123: coercive caller
?>
// File: strict.php
<?php
declare(strict_types=1);
require __DIR__ . '/lib.php';
try {
echo takesInt('123');
} catch (TypeError $e) {
echo 'TypeError';
}
?>
2Explain the difference between == and === in PHP, including pitfalls with numeric strings, arrays, objects, and in_array().
PHP == uses loose comparison, including type conversion where applicable; === requires matching types and values without scalar coercion. Numeric strings can compare numerically, so do not use loose comparison for values that require exact type-sensitive identity. Arrays are equal with == when they contain the same key/value pairs under the comparison rules, regardless of pair order. Array === additionally requires the same order and matching key/value types. Object == compares properties of objects of the same class; object === requires the same instance. in_array is loose by default; pass true as its third argument for strict checks. Loose number/string behavior changed in PHP 8, so avoid relying on historical coercion examples without a version.
3How do mixed, void, and never differ as PHP return types, and what guarantees do they provide?
mixed permits any returned value, including null. void declares that a function does not return a meaningful value: it may fall through or use return; but cannot return an expression. A normal call to a void function evaluates to null. never, available since PHP 8.1, means the function never returns normally to its caller. It may throw, terminate execution with exit, or remain in an infinite loop; it need not eventually terminate the process. A never function cannot contain a return statement. The key difference is that a void function may finish normally and its caller continues, while a never function has no normal continuation.
<?php
function parse(string $raw): mixed {
return json_decode($raw, true);
}
function logMessage(string $message): void {
error_log($message);
return; // allowed
}
function fail(string $message): never {
throw new RuntimeException($message);
}
logMessage('starting');
fail('stop');
echo 'unreachable';
4What is iterable in PHP, and how does it relate to array, Traversable, Iterator, and Generator?
iterable means array|Traversable. A Generator implements Iterator, which extends Traversable, so it satisfies iterable. User-defined classes provide Traversable behavior through Iterator or IteratorAggregate rather than directly implementing Traversable in a concrete class. Iterator exposes methods that control iteration; IteratorAggregate supplies an iterator. A generator function containing yield returns a Generator. There is an important distinction: foreach can also enumerate visible properties of an ordinary object that does not implement Traversable, but that object does not thereby satisfy an iterable parameter declaration.
function printItems(iterable $items): void {
foreach ($items as $item) {
echo $item, PHP_EOL;
}
}
printItems(['a', 'b']);
function letters(): Generator {
yield 'x';
yield 'y';
}
printItems(letters());
5What are public, protected, and private visibility rules in PHP, and how do they interact with inheritance and traits?
Public members are accessible wherever the object or class can be accessed. Protected members are accessible within the declaring class and the related parent/child class hierarchy, rather than arbitrary external code. Private members are accessible within their declaring class; a subclass cannot use a parent's private member as its inherited API. An ordinary overriding method cannot narrow the inherited visibility, although constructors have special inheritance rules and must not be treated as an unconditional example of that rule. Trait members are composed into the using class and follow its visibility rules. The as adaptation can create an alias and/or adjust a trait method's visibility; adding an alias does not itself remove the original name.
<?php
class Base {
public function a() {}
protected function b() {}
private function c() {}
}
trait Logs {
public function log(string $message): void {}
}
class Child extends Base {
use Logs { log as protected writeLog; }
public function test(): void {
$this->a(); // OK
$this->b(); // OK
// $this->c(); // Not accessible: private to Base
$this->writeLog('ok'); // OK inside class, protected alias
}
}
$child = new Child();
$child->a();
// $child->b(); // Not accessible from outside
// $child->writeLog('x'); // Not accessible from outside
6How do traits work in PHP, and how are conflicts resolved when multiple traits define the same method?
Traits provide horizontal code reuse: a class imports trait members with use, and a trait cannot be instantiated by itself. A method declared in the class takes precedence over trait methods, which take precedence over inherited parent methods. If multiple traits supply the same method and the conflict is not otherwise resolved by a class method, use insteadof to choose the implementation. Use as to give a trait method an additional name and optionally a different visibility. An alias does not rename or remove the original method. Resolving collisions and keeping the resulting public API clear are separate responsibilities.
<?php
trait JsonLogger {
public function log(string $message): void {
echo "json:$message\n";
}
}
trait FileLogger {
public function log(string $message): void {
echo "file:$message\n";
}
}
class Service {
use JsonLogger, FileLogger {
JsonLogger::log insteadof FileLogger;
FileLogger::log as logToFile;
JsonLogger::log as protected logJson;
}
}
$service = new Service();
$service->log('hello');
$service->logToFile('hello');
// $service->logJson('hello'); // not accessible from outside; protected alias
7What are PHP enums, including pure and backed enums, and how do they improve domain modeling?
PHP enums define a closed set of named cases for a type, such as order statuses or user roles. Pure enums have named cases only; backed enums give each case a unique string or int value, useful for storage, APIs, or interoperability. Enums improve domain modeling by replacing fragile raw strings, integers, or constants with type-safe, self-documenting values. PHP enums can also have methods and implement interfaces; all enums support listing their cases, and backed enums support conversion to and from their scalar values.
enum OrderState
{
case Draft;
case Paid;
case Shipped;
}
enum PaymentStatus: string
{
case Pending = 'pending';
case Complete = 'complete';
case Failed = 'failed';
}
$status = PaymentStatus::tryFrom('complete');
var_dump($status === PaymentStatus::Complete);
8How do scalar, nullable, union, intersection, and DNF types express PHP API contracts, and when should each be used?
PHP declarations describe accepted and returned values. Scalar declarations such as int, float, string and bool select a value category; whether compatible scalar coercion is allowed also depends on strict_types and context. ?T or T|null admits null. Unions, available since PHP 8.0, admit alternatives such as int|string. Pure intersections, available since PHP 8.1, require an object to satisfy all listed class/interface types, such as Cacheable&JsonSerializable; int&string is not a valid intersection. DNF types, available since PHP 8.2, combine alternatives and parenthesized intersections, for example (Cacheable&JsonSerializable)|string. Choose the narrowest understandable contract that represents valid inputs rather than adding unnecessary alternatives.
<?php
interface Cacheable {}
class Report implements Cacheable, JsonSerializable {
public function jsonSerialize(): mixed { return []; }
}
function findName(int $id): ?string {
return $id > 0 ? 'Alice' : null;
}
function normalize(int|string $id): string {
return (string) $id;
}
function store(Cacheable&JsonSerializable $item): void {
// must satisfy both interfaces
}
function handle((Cacheable&JsonSerializable)|string $input): void {
// either an object satisfying both contracts, or a string
}
9What is the difference between callable, Closure, first-class callables, invokable objects, and callable arrays in PHP?
callable describes invocable values, including function-name strings, Closure objects, accessible method references and objects with __invoke. Closure is a concrete callable object: it may originate from an anonymous function or from a named function/method converted to a Closure. First-class callable syntax, such as strlen(...) or $object->method(...), creates a Closure without invoking the target. A callable array [$object, 'method'] refers to an accessible instance method; [ClassName::class, 'method'] requires an appropriate accessible static method in modern PHP. is_callable checks callability in the relevant scope. callable is a broad contract, while Closure is a specific representation.
class Greeter {
public function hello(string $name): string { return "Hello $name"; }
public function __invoke(string $name): string { return "Hi $name"; }
}
$g = new Greeter();
$closure = fn(string $name) => "Hey $name";
$arrayCallable = [$g, 'hello'];
$firstClass = $g->hello(...);
$invokable = $g;
foreach ([$closure, $arrayCallable, $firstClass, $invokable] as $cb) {
echo $cb('Sam'), PHP_EOL;
}
10How does PHP handle integer overflow, floating-point precision, and exact decimal calculations?
PHP integers are platform-sized, with limits exposed by constants such as `PHP_INT_MAX`. PHP does not automatically keep native integer results as arbitrary-precision integers after overflow; overflowing integer arithmetic can become `float` or otherwise lose exact integer semantics. PHP floats are binary floating-point values, so many decimal fractions, such as `0.1`, are approximate. For exact decimal or money calculations, avoid floats and use an exact approach such as integer minor units where appropriate, or fixed/arbitrary-precision decimal tools such as BCMath. GMP is useful for arbitrary-precision integer arithmetic.
11Explain PHP references (&), copy-on-write, and how references can create unexpected side effects.
PHP variables are represented internally by zvals. For values such as arrays and strings, PHP uses copy-on-write: assigning `$b = $a` usually shares the same underlying value until one variable is modified, at which point PHP separates/copies it. A reference created with `&` makes variables aliases to the same container, so changing one changes the other and can bypass the separation behavior people expect from normal assignment. Passing by reference lets a function modify the caller’s variable. References can cause surprising side effects, especially with `foreach ($array as &$value)`: the loop variable remains a reference to the last element after the loop, so reusing it later can accidentally overwrite that element unless you `unset($value)`.
12How do object handles, assignment, clone, __clone, shallow copy, and deep copy work in PHP?
In PHP, an object variable stores a handle to an object, not a full object copy. Assigning `$b = $a` copies the handle, so both variables refer to the same object; mutating the object through either variable is visible through the other, without using `&`. `clone $a` creates a new object instance with a shallow copy of the original object’s properties. If a property is itself an object, the cloned object’s property still points to the same nested object unless you explicitly clone it. `__clone()` is called on the new object after the shallow copy and is the place to reset IDs, detach resources, or perform a deep copy of nested objects. A deep copy means recursively copying the nested mutable objects that should not be shared.
class Address {
public function __construct(public string $city) {}
}
class User {
public function __construct(public string $name, public Address $address) {}
public function __clone() {
$this->address = clone $this->address; // make nested object independent
}
}
$u1 = new User('Ann', new Address('Paris'));
$u2 = $u1; // same object handle
$u2->name = 'Beth';
echo $u1->name, PHP_EOL;
$u3 = clone $u1; // new User object; __clone deep-clones address
$u3->address->city = 'Rome';
echo $u1->address->city, PHP_EOL;
echo $u3->address->city, PHP_EOL;
13How would you troubleshoot database connection exhaustion and design connection management for PHP-FPM and long-running workers?
To troubleshoot connection exhaustion, I would compare database connection limits and current sessions with total PHP concurrency: each active PHP-FPM child can hold its own database connection, and multiple app pools, hosts, cron jobs, queue workers, migrations, and deploys multiply demand. I would inspect database session views, connection states, idle-in-transaction sessions, slow queries, lock waits, error logs, pool metrics, and recent traffic or worker-count changes. For PHP-FPM, keep max_children and per-host concurrency within the database budget, use short request-scoped transactions/connections, commit or roll back promptly, and avoid holding connections during slow external calls. Persistent PDO connections can reduce connect overhead but may pin many idle connections per FPM process and retain session state, so they require caution. A pooler or proxy such as PgBouncer can multiplex many PHP clients onto fewer database server connections when the application is compatible. Long-running workers need explicit connection lifecycle handling: health checks, reconnect after idle timeouts/restarts/failover/forks, reset session state, and retry only safe or idempotent work.
Connection-budget example:
- 6 application servers
- 2 PHP-FPM pools on EACH server
- pm.max_children = 40 for EACH pool
- Assume at most 1 database connection per child
Potential request connections = 6 * 2 * 40 = 480.
If 40 were the TOTAL children per server across both pools, it would be 240.
Budget separately for queue workers, cron, deployments and administrators.
A limit of 300 database connections cannot accommodate 480 simultaneous connections.
14How would you handle read replicas, read/write splitting, replication lag, and primary database failover in PHP?
Separate primary and replica connections according to operation semantics, not merely whether SQL begins with SELECT: locking reads, writes and transactions that require primary consistency must use the primary. Read-only transactions may use a suitable replica when their consistency requirements allow it. Asynchronous replication can lag. Laravel's sticky setting routes later reads to the write connection during the current request after a write; a fixed pinning interval across requests is only a mitigation, not an unconditional read-your-writes guarantee. For a guarantee, read from the primary or wait until a replica has applied the required write position, with timeout and primary fallback. Proxy behavior differs: RDS Proxy uses separate reader/writer endpoints and is not a general automatic per-statement read/write splitter. On failover, discard stale connections and reconnect with bounded backoff. A lost commit response has an ambiguous outcome: do not blindly replay non-idempotent writes; use idempotency keys or reconcile the result first.
15How do database migrations and schema changes get deployed safely with zero or minimal downtime in PHP applications?
Use expand-contract changes that tolerate the old and new application versions during a rolling deployment. First add compatible structures, then deploy code that can use both representations. If dual-writing is needed, keep it consistent and design resumable, throttled backfills that do not overwrite newer writes. Validate the copied data and switch reads before retiring old code paths. Online DDL reduces particular blocking risks but is not universally lock-free: inspect the database/version-specific locks, transaction restrictions, resource use and replication impact. PostgreSQL CREATE INDEX CONCURRENTLY, for example, has restrictions and can leave an invalid index after failure. Remove old columns only after old application instances and the required rollback window are gone. After destructive contract changes, rolling back to code that needs removed data is no longer automatically safe.
<?php
// Step 1: Migration (Expand) -> Add new column as nullable
Schema::table('users', function (Blueprint $table) {
$table->string('full_name')->nullable()->after('name');
});
// Step 2: Code deployment -> Write to both, read with fallback
class User extends Model {
public function setNameAttribute($value) {
$this->attributes['name'] = $value;
$this->attributes['full_name'] = $value; // Dual write
}
}
// Step 3: Background worker backfills existing rows in small batches
// Step 4: Code deployment -> Read/write exclusively from 'full_name'
// Step 5: Migration (Contract) -> Drop legacy 'name' column