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 is weak/coercive by default for scalar type declarations: when a function declares scalar parameter or return types, PHP may convert compatible values such as numeric strings to numbers instead of rejecting them. `declare(strict_types=1)` makes scalar type checking strict for that file, so incompatible scalar values are not coerced and a `TypeError` is thrown. The directive is file-scoped: for user-defined function arguments, the caller's file controls strictness; for return statements, the file where the function is defined controls return-value strictness. It does not make PHP globally strict or disable all conversions in the language.
<?php
// lib.php
function takesInt(int $x): int {
return $x;
}
// weak.php, no strict_types
echo takesInt('123'); // coerces string to int
// strict.php
declare(strict_types=1);
echo takesInt('123'); // TypeError
2Explain the difference between == and === in PHP, including pitfalls with numeric strings, arrays, objects, and in_array().
`==` performs loose comparison, so PHP may juggle types before comparing; `===` performs strict comparison, requiring both the same value and the same type with no coercion. Numeric strings are a common pitfall because they can be treated as numbers in loose comparisons. Arrays and objects have their own comparison rules: arrays are compared by keys and values, while object `==` compares same-class objects by properties and object `===` requires the exact same instance. `in_array()` uses loose comparison by default, so pass `true` as the third argument when type must match.
3How do mixed, void, and never differ as PHP return types, and what guarantees do they provide?
As return types, `mixed`, `void`, and `never` make different promises. `mixed` means the function may return any value, including `null`. `void` means the function is not intended to return a meaningful value; it may use `return;` but cannot return an expression. `never` means the function does not return normally to its caller: it must always throw, exit, or otherwise terminate control flow. `never` is stronger than `void` because execution cannot continue after a normal call to a `never` function.
<?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?
In PHP, `iterable` is a type declaration meaning “an `array` or an object implementing `Traversable`.” `Traversable` is the base marker interface for objects usable by `foreach`; userland code normally implements `Iterator` or `IteratorAggregate`, which extend `Traversable`. A `Generator` is the object returned from a generator function using `yield`; it implements `Iterator`, so it is also `Traversable` and accepted anywhere `iterable` is required.
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?
In PHP, public members are accessible from anywhere, protected members are accessible from the declaring class and subclasses, and private members are accessible only from the class that declares them. In inheritance, subclasses can use inherited public and protected members, but a parent’s private members are not accessible as inherited API. When overriding methods, a child generally cannot reduce visibility; for example, a public parent method must remain public. Traits are composed into the using class, and trait members follow PHP visibility rules. Trait method visibility can also be adapted or aliased with as when importing the trait.
<?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 are PHP’s mechanism for horizontal code reuse: a class can use one or more traits, and the trait’s members are incorporated into the class. Traits are not standalone classes and cannot be instantiated directly. If multiple traits provide methods with the same name, PHP requires explicit conflict resolution. The insteadof operator chooses which trait method is used for the conflicting name. The as operator can create an alias for a trait method and can also change the imported method’s visibility. Class methods override trait methods, and trait methods take precedence over inherited parent methods.
<?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 type declarations describe the values an API accepts or returns. Scalar types such as `int`, `float`, `string`, and `bool` are used when exactly one primitive kind is expected. Nullable types, written `?T` or `T|null`, allow either a value of type `T` or `null`. Union types such as `A|B` allow one of several alternatives. Intersection types such as `A&B` require a value to satisfy all listed types, commonly multiple interfaces. DNF types combine unions and intersections, for example `(A&B)|C`, to express more complex contracts. Each should be used when it matches the real API promise, avoiding unnecessarily broad or confusing types.
<?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` is a broad type/contract for any value PHP can invoke, such as a function-name string, a `Closure`, an invokable object, or a method reference array like `[$object, 'method']` or `[ClassName::class, 'method']`. A `Closure` is a concrete anonymous-function object and is itself callable. First-class callable syntax, such as `$obj->method(...)` or `strlen(...)`, creates a `Closure` from a callable expression. An invokable object is callable because it defines `__invoke()`. Callable arrays are the traditional two-element method-reference form. `is_callable()` can be used to check callability at runtime.
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.
Example:
- 6 application servers
- 40 PHP-FPM max_children per server
- 2 separate FPM pools that can hit the database
Potential concurrent PHP request DB connections = 6 * 40 * 2 = 480
If PostgreSQL max_connections is 300 and queues/cron/admin tools also connect,
connection exhaustion is expected during traffic spikes.
14How would you handle read replicas, read/write splitting, replication lag, and primary database failover in PHP?
Handling read replicas and failover in PHP requires splitting read and write traffic, mitigating replication lag, and managing connection disruptions during failover. At the application level, database configuration or middleware routes read queries (`SELECT`) to read replicas and write queries (`INSERT`, `UPDATE`, `DELETE`) to the primary instance (alternatively managed via database proxies like ProxySQL or AWS RDS Proxy). Because replication is asynchronous, replication lag can cause stale reads. To guarantee 'read-your-writes' consistency, the application should route reads within an active transaction to the primary, and temporarily stick subsequent reads to the primary for a specific user session or request lifecycle immediately following a write. During primary failover, modern database clusters promote a replica and update DNS/proxy endpoints. In PHP, the connection layer must catch connection loss and read-only errors, drop stale PDO sockets, and apply controlled retries with exponential backoff.
15How do database migrations and schema changes get deployed safely with zero or minimal downtime in PHP applications?
Deploying zero-downtime database migrations in PHP applications requires decoupling schema evolution from application code deployments using the Expand and Contract (Parallel Run) pattern alongside non-blocking online DDL practices. In the Expand phase, backward-compatible schema changes are applied (e.g., adding nullable columns, new tables, or concurrent indexes using tools like pt-online-schema-change, gh-ost, or native online DDL in MySQL/PostgreSQL). The application is then deployed to support dual-writing (writing to both old and new schema structures) while reading from either. Historical records are then backfilled asynchronously in throttled background batches to prevent locking tables or inducing replication lag. In the Contract phase, once data consistency is validated and reads are switched completely to the new structure, a code deployment removes references to the old schema. Finally, a cleanup migration drops legacy columns or tables. Safe rollback requires that each intermediate schema state remains fully compatible with both the previous and incoming application versions during rolling releases.
<?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