Skip to content

PHP interview questions

Status: Complete for the PHP interview core. Last reviewed 2026-08-28.

Example answers provide a solid core response followed by a senior extension. They model credible spoken answers, not grading rubrics or uniquely correct scripts.

PHP-TYPES-01 — Explain strict typing from the call site

Section titled “PHP-TYPES-01 — Explain strict typing from the call site”

What exactly does declare(strict_types=1) affect, whose declaration controls the behavior, and what does it leave unchecked?

PHP-TYPES-02 — Design a precise boundary type

Section titled “PHP-TYPES-02 — Design a precise boundary type”

An API payload contains a status, optional retry time, and a list of item identifiers. How would you move it from untrusted arrays into precisely typed application code?

PHP-TYPES-03 — Explain variance through substitutability

Section titled “PHP-TYPES-03 — Explain variance through substitutability”

Explain parameter contravariance and return covariance using a PHP interface. Why are the opposite directions unsafe?

PHP-TYPES-04 — Choose an enum or another model

Section titled “PHP-TYPES-04 — Choose an enum or another model”

When is a backed enum a good model, and when should the concept instead be a value object or database-backed entity?

PHP-TYPES-05 — State what readonly does not guarantee

Section titled “PHP-TYPES-05 — State what readonly does not guarantee”

What invariant does a readonly property or class enforce, and how can mutable state still change through it?

PHP-TYPES-06 — Combine native and static-analysis types

Section titled “PHP-TYPES-06 — Combine native and static-analysis types”

How would you divide responsibility between native PHP declarations, PHPDoc/static-analysis types, runtime validation, and tests?

PHP-OBJECTS-01 — Explain object assignment without “passed by reference”

Section titled “PHP-OBJECTS-01 — Explain object assignment without “passed by reference””

What happens when a PHP object is assigned to another variable or passed to a function, and how is that different from an explicit PHP reference?

PHP-OBJECTS-02 — Design safe clone semantics

Section titled “PHP-OBJECTS-02 — Design safe clone semantics”

A domain object contains nested mutable objects and external-resource collaborators. What should clone mean, and when should cloning be avoided?

PHP-OBJECTS-03 — Contrast self, static, and $this

Section titled “PHP-OBJECTS-03 — Contrast self, static, and $this”

Contrast self::, static::, parent::, and $this, including one failure mode caused by late static binding.

PHP-OBJECTS-04 — Choose inheritance, a trait, or composition

Section titled “PHP-OBJECTS-04 — Choose inheritance, a trait, or composition”

You need to share behavior across several services. How do you decide among an abstract base class, a trait, and a composed collaborator?

PHP-OBJECTS-05 — Use finality to protect an invariant

Section titled “PHP-OBJECTS-05 — Use finality to protect an invariant”

When does final protect a real invariant, and when does it merely make extension or testing harder?

PHP-OBJECTS-06 — Diagnose a magic-method boundary

Section titled “PHP-OBJECTS-06 — Diagnose a magic-method boundary”

A serializer unexpectedly executes database queries through model property access. How would you reason about and redesign that boundary?

Functions, closures, callables, and generators

Section titled “Functions, closures, callables, and generators”

PHP-CALLABLES-01 — Explain capture by value and reference

Section titled “PHP-CALLABLES-01 — Explain capture by value and reference”

Contrast closure capture by value, closure capture by reference, and arrow-function capture. What happens when the captured value is an object?

PHP-CALLABLES-02 — Choose a callable representation

Section titled “PHP-CALLABLES-02 — Choose a callable representation”

Choose among a plain function, closure, first-class callable, string/array callable, and invokable object for application behavior.

PHP-CALLABLES-03 — Treat named arguments as compatibility surface

Section titled “PHP-CALLABLES-03 — Treat named arguments as compatibility surface”

Why can renaming a parameter be a breaking change, and what risks appear when forwarding unpacked arguments?

PHP-CALLABLES-04 — Explain object mutation without reference parameters

Section titled “PHP-CALLABLES-04 — Explain object mutation without reference parameters”

Why can a function mutate an object passed by value, and when would an explicit reference parameter actually change behavior?

PHP-CALLABLES-05 — Design a safe generator pipeline

Section titled “PHP-CALLABLES-05 — Design a safe generator pipeline”

When does a generator improve a production data pipeline, and which resource, failure, and consistency risks remain?

PHP-CALLABLES-06 — Diagnose captured state in a worker

Section titled “PHP-CALLABLES-06 — Diagnose captured state in a worker”

A long-running worker gradually retains memory and occasionally uses the previous job’s tenant context. How could closures participate, and how would you prove it?

Arrays, values, references, and comparison

Section titled “Arrays, values, references, and comparison”

PHP-ARRAYS-01 — Predict array key normalization

Section titled “PHP-ARRAYS-01 — Predict array key normalization”

Predict how integer-like strings, gaps, insertion order, merge, and union operations affect a PHP array’s keys and list shape.

PHP-ARRAYS-02 — Diagnose a JSON shape change

Section titled “PHP-ARRAYS-02 — Diagnose a JSON shape change”

Why can filtering a PHP list make json_encode() emit an object, and where should the repair live?

PHP-ARRAYS-03 — Explain copy-on-write and peak memory

Section titled “PHP-ARRAYS-03 — Explain copy-on-write and peak memory”

Why can assigning a large array be cheap while the next mutation exhausts memory? What does and does not get copied?

PHP-ARRAYS-04 — Diagnose reference leakage

Section titled “PHP-ARRAYS-04 — Diagnose reference leakage”

Explain the by-reference foreach leakage problem and distinguish it from object-handle sharing and copy-on-write.

PHP-ARRAYS-05 — Choose strict comparison and normalization

Section titled “PHP-ARRAYS-05 — Choose strict comparison and normalization”

When should code use strict comparison, when should it normalize first, and why is loose comparison dangerous at trust boundaries?

PHP-ARRAYS-06 — Distinguish missing, null, false, and empty

Section titled “PHP-ARRAYS-06 — Distinguish missing, null, false, and empty”

Contrast isset(), array_key_exists(), empty(), and null coalescing for a request or configuration array.

Errors, exceptions, shutdown, and recoverability

Section titled “Errors, exceptions, shutdown, and recoverability”

PHP-ERRORS-01 — Classify Throwable, Exception, and Error

Section titled “PHP-ERRORS-01 — Classify Throwable, Exception, and Error”

What belongs to each category, which engine failures are catchable, and why does catchable not mean recoverable?

PHP-ERRORS-02 — Design an error-conversion policy

Section titled “PHP-ERRORS-02 — Design an error-conversion policy”

When should a custom error handler convert diagnostics to ErrorException, and which failures remain outside that mechanism?

PHP-ERRORS-03 — Place exception translation and reporting

Section titled “PHP-ERRORS-03 — Place exception translation and reporting”

Where should infrastructure exceptions become application or transport failures without losing causal evidence?

What can a shutdown function observe or clean up, and why is it not a general recovery mechanism?

PHP-ERRORS-05 — Keep destructors out of correctness

Section titled “PHP-ERRORS-05 — Keep destructors out of correctness”

Why are destructors unsuitable for important commits, delivery, acknowledgements, or distributed-lock release?

PHP-ERRORS-06 — Decide whether a failed operation is retryable

Section titled “PHP-ERRORS-06 — Decide whether a failed operation is retryable”

An external API call times out after local state changed. How do you classify the outcome and decide whether retry is safe?

Runtime, memory, OPcache, and process models

Section titled “Runtime, memory, OPcache, and process models”

PHP-RUNTIME-01 — Trace source through OPcache execution

Section titled “PHP-RUNTIME-01 — Trace source through OPcache execution”

Trace a PHP file from source to execution with OPcache enabled. What does OPcache retain, and what deployment assumptions can make it stale?

PHP-RUNTIME-02 — Explain zvals without overpromising internals

Section titled “PHP-RUNTIME-02 — Explain zvals without overpromising internals”

Use zvals, refcounts, copy-on-write, object handles, references, and cycle collection to explain observable value behavior.

PHP-RUNTIME-03 — Diagnose PHP memory versus RSS

Section titled “PHP-RUNTIME-03 — Diagnose PHP memory versus RSS”

A worker’s RSS grows while memory_get_usage() stabilizes. Give plausible explanations and a measurement sequence.

PHP-RUNTIME-04 — Size and diagnose an FPM pool

Section titled “PHP-RUNTIME-04 — Size and diagnose an FPM pool”

How would you choose pm.max_children and diagnose rising tail latency when the pool is saturated?

PHP-RUNTIME-05 — Prevent long-running state leakage

Section titled “PHP-RUNTIME-05 — Prevent long-running state leakage”

What state normally disappears between FPM requests but may survive queue jobs or Octane requests, and how should code handle it?

PHP-RUNTIME-06 — Decide whether JIT is relevant

Section titled “PHP-RUNTIME-06 — Decide whether JIT is relevant”

When is PHP JIT likely to help, and how would you determine whether it is the right optimization for a Laravel workload?

PHP-COMPOSER-01 — Contrast install, update, and the lock

Section titled “PHP-COMPOSER-01 — Contrast install, update, and the lock”

Explain dependency resolution and reproducibility for an application and a reusable library. Why is update in deployment dangerous?

PHP-COMPOSER-02 — Debug a dependency-resolution conflict

Section titled “PHP-COMPOSER-02 — Debug a dependency-resolution conflict”

Composer cannot select compatible versions after a framework upgrade. Give a diagnosis and remediation sequence without deleting the lock blindly.

PHP-COMPOSER-03 — Explain what PSR-4 guarantees

Section titled “PHP-COMPOSER-03 — Explain what PSR-4 guarantees”

What does PSR-4 map and require, what does it leave to the autoloader/application, and why can code work locally but fail on Linux?

PHP-COMPOSER-04 — Choose an autoloader optimization

Section titled “PHP-COMPOSER-04 — Choose an autoloader optimization”

Contrast optimized classmaps, authoritative classmaps, and APCu autoload caching across development and production.

PHP-COMPOSER-05 — Secure a Composer build

Section titled “PHP-COMPOSER-05 — Secure a Composer build”

Threat-model Composer plugins, scripts, repositories, platform overrides, advisories, and build credentials.

PHP-COMPOSER-06 — Use PSRs without architecture theater

Section titled “PHP-COMPOSER-06 — Use PSRs without architecture theater”

When does adopting a PHP-FIG PSR improve a Laravel application’s boundary, and when is an adapter merely ceremony?

PHP-MIGRATION-01 — Plan a staged PHP upgrade

Section titled “PHP-MIGRATION-01 — Plan a staged PHP upgrade”

Plan an upgrade from an older supported or end-of-life PHP/Laravel estate to the current baseline without combining every risk into one release.

PHP-MIGRATION-02 — Ratchet deprecations safely

Section titled “PHP-MIGRATION-02 — Ratchet deprecations safely”

How should a team collect, prioritize, and eliminate deprecations without turning production diagnostics into outages?

PHP-MIGRATION-03 — Add native types to legacy code

Section titled “PHP-MIGRATION-03 — Add native types to legacy code”

How would you introduce parameter, return, and property types into coercive, docblock-heavy code while preserving behavior?

PHP-MIGRATION-04 — Replace dynamic properties and annotations

Section titled “PHP-MIGRATION-04 — Replace dynamic properties and annotations”

Choose safe replacements for dynamic properties and parser-based annotations, including cases where temporary compatibility is justified.

PHP-MIGRATION-05 — Modernize tests and static analysis

Section titled “PHP-MIGRATION-05 — Modernize tests and static analysis”

How would you upgrade PHPUnit/Pest and introduce stricter static analysis without mistaking tool migration for better confidence?

PHP-MIGRATION-06 — Keep a mixed-version deployment compatible

Section titled “PHP-MIGRATION-06 — Keep a mixed-version deployment compatible”

What can break when old and new PHP/Laravel workers coexist, and how do you design code, schema, queues, and rollback for that window?

PHP-TYPES-01 — Example answer

Solid response: strict_types is a per-file rule for scalar coercion at user-defined function call sites. The file making the call decides whether scalar arguments such as a numeric string may be coerced. It is not a permanent property of the function’s defining file. Return types are checked according to the file where the function is declared. Object subtype checks do not become “stricter,” and int is still accepted for float. A mismatch raises TypeError.

It does not validate HTTP or JSON input, recursively type an array, change database-driver conversion, or make internal functions uniformly follow userland rules. I still parse and validate untrusted data before constructing typed values.

Senior extension: The practical trap is boundary migration. Adding strict declarations to a library does not force coercive callers to become strict, so I would enable strict mode consistently in application files and use static analysis to locate weak edges. I would test adapters that cross framework, extension, serialization, and legacy boundaries because those APIs can have their own conversion behavior. Strict typing improves local contracts; it is not proof that the system’s external data is trustworthy.

PHP-CALLABLES-01 — Example answer

Solid response: An explicit closure captures listed variables with use. By-value capture records the value when the closure is created; later rebinding of the outer variable is not observed. By-reference capture with & aliases that variable, so later changes are shared. An arrow function automatically captures outer variables it uses by value.

If the value is an object, by-value capture copies its handle. The outer and captured values still reach the same instance, so mutating that object is visible even though rebinding the outer variable is not.

Senior extension: I avoid reference capture for hidden workflow state because invocation order and retries become significant. In a long-running process, a closure can also retain $this, a request container, or a large graph. A static closure avoids implicit $this capture. For durable state I prefer an explicit invokable object whose fields and reset/lifetime rules can be reviewed and tested.

PHP-CALLABLES-02 — Example answer

Solid response: I use a plain function for stateless namespace-level transformation, a closure for small local behavior with explicit capture, and a first-class callable when adapting an existing method into a Closure. An invokable object fits named policy with dependencies or state. String/array callables are supported but are weaker under refactoring and can depend on visibility at the call scope.

If behavior must be stored as a property, I normalize it to Closure because callable is not a valid property type and can be scope-dependent.

Senior extension: I also ask about lifetime and serialization. Capturing a service graph in a callback retained by a worker may leak state; attempting to serialize executable callbacks into queue payloads is brittle and unsafe. For queued work I serialize data and resolve a named handler in the consumer. The representation should expose ownership and dependencies, not merely satisfy is_callable() today.

PHP-CALLABLES-03 — Example answer

Solid response: Named arguments bind to the parameter’s name, so callers depend on that name just as positional callers depend on order. Renaming it can break runtime calls even if the type and position are unchanged. I use names confidently for application APIs I control, but avoid assuming vendor parameter names are stable unless documented.

With ...$arguments, integer keys are positional while string keys become named arguments. Unknown names, duplicate bindings, or a downstream rename can make a forwarding wrapper fail.

Senior extension: A generic forwarding wrapper unintentionally inherits the complete compatibility surface of the target. I would define and validate an application-owned options shape, then map it explicitly. If a signature has many unrelated optional flags, a typed options object or separate operations often evolves more safely than named arguments masking excessive responsibility.

PHP-CALLABLES-04 — Example answer

Solid response: Passing an object by value passes its object-handle value. The parameter and caller variable reach the same instance, so calling a mutating method or changing a public property changes that instance. Assigning a different object to the local parameter does not rebind the caller’s variable.

A by-reference parameter aliases the caller variable itself. Reassigning the parameter can then replace what the caller variable contains. That is rarely needed for objects and should be explicit when it is the API’s purpose.

Senior extension: I prefer returning a new result to output parameters because reference mutation obscures data flow, complicates static analysis, and interacts poorly with retries. If a function mutates an object intentionally, I make that visible in the method and type design rather than adding &, which would introduce a different and usually stronger coupling.

PHP-CALLABLES-05 — Example answer

Solid response: A generator helps when the consumer can process values incrementally and building the whole collection would dominate memory or delay the first result. Its frame resumes around each yield; it does not reduce total work or make blocking I/O asynchronous.

Exceptions may occur during iteration rather than generator creation. Suspended locals can retain database cursors, files, objects, or transactions, and partial consumption may leave external output already emitted.

Senior extension: I define who owns resource closure, what partial failure means, and whether the source can be resumed. I avoid holding a database transaction while a slow consumer performs network I/O. For durable large jobs I often page by a stable key, checkpoint progress, and pass plain values into the slow stage. I measure peak memory, source query behavior, throughput, and recovery rather than assuming yield solved the pipeline.

PHP-CALLABLES-06 — Example answer

Solid response: A callback registered in a singleton or static registry may capture $this, a tenant context object, or a job payload. Because the process survives, that graph survives too. Later jobs may invoke the old callback or mutate the same captured object, causing both memory retention and cross-job state leakage.

I would reproduce multiple jobs in one process, log callback registration and tenant identity, compare object IDs, and profile retained references after each job. Restarting per job would hide rather than fix it.

Senior extension: The repair is lifetime alignment: register callbacks once only if they are stateless, resolve job-scoped data at invocation, unregister temporary listeners in finally, and avoid capturing request/job containers. I would add a repeated-job isolation test and memory slope measurement. Worker recycling remains a safety valve, not the correctness boundary.

PHP-OBJECTS-01 — Example answer

Solid response: An object variable contains a handle identifying an instance. Assignment or argument passing copies that handle, so both variables initially reach the same object and mutations are visible through both. Reassigning one variable to a new object does not reassign the other.

An explicit PHP reference created with & aliases variable containers, so rebinding through one name can affect the other. That is a different feature. I do not need & to let a function mutate an object, and saying objects are simply “passed by reference” hides the important rebinding distinction.

Senior extension: I separate instance identity from domain identity and value equality. === checks the same instance; == performs PHP’s structural object comparison, which may not match domain rules. A persisted entity needs a durable ID, while a value object should usually expose intentional equality. This vocabulary helps diagnose leaked mutation without introducing unnecessary references.

PHP-OBJECTS-02 — Example answer

Solid response: PHP clone creates a new outer object but nested object properties still point to the same instances unless __clone() replaces them. The owning class must define which nested values are copied, shared, reset, or prohibited. An external connection or service should normally remain a collaborator rather than be cloned; an entity ID may need clearing or cloning may be invalid altogether.

I prefer named operations such as duplicateAsDraft() or withAddress() when the business meaning matters. They can construct a valid result instead of exposing a mechanical copy operation.

Senior extension: “Deep clone everything” fails for cycles, deliberately shared graph nodes, resources, and identity-bearing entities. I would test both equality and non-aliasing of the fields the copy contract promises. For immutable value objects, returning a new instance with selected changes is clear; PHP 8.5 clone-with syntax can reduce ceremony but does not change the shallow graph semantics.

PHP-OBJECTS-03 — Example answer

Solid response: $this is the current instance. self:: resolves to the class where the method was declared. parent:: calls into that lexical class’s parent. static:: uses the runtime called class through late static binding, which supports derived named constructors and overridable static hooks.

A failure appears when a base implementation uses new static() but a child changes its constructor requirements. The base method now instantiates the child with arguments that no longer fit. new self() would avoid that polymorphism but also change the contract.

Senior extension: I use late binding only when the hierarchy deliberately promises derived-type preservation, often with a static return type and controlled construction. If base behavior depends on subclass static properties, private-method edge cases, or undocumented constructors, composition is usually easier to reason about. I verify the runtime called class rather than guessing from the source line containing the method.

PHP-OBJECTS-04 — Example answer

Solid response: An abstract base class fits a stable substitutable family that needs shared state or a template algorithm and can accept single-inheritance coupling. A trait is compile-time member reuse; it fits small mechanical behavior but is not a collaborator or type. Composition fits a real capability with its own contract, state, lifecycle, or alternative implementations.

I would not choose inheritance merely to reuse code. I ask whether every child can honor the base contract, whether protected hooks are intentional, and whether the shared behavior has a dependency that should be explicit.

Senior extension: Traits that assume host properties or methods create hidden structural coupling, while conflict operators only solve names, not semantics. Deep inheritance makes base changes risky and test fixtures artificial. My default for business behavior is a small interface plus injected implementation; I retain inheritance where the framework or domain genuinely defines a stable subtype relation.

PHP-OBJECTS-05 — Example answer

Solid response: Finality is valuable when overriding only part of an operation could violate construction, authorization, equality, or a state transition. A final implementation behind an interface can preserve those invariants while callers depend on the abstraction. A final method can close one critical algorithm while leaving documented hooks elsewhere.

It is merely restrictive when the class has no invariant at risk and consumers have a legitimate supported extension need. It can also expose a test design that relies on subclassing concrete classes rather than substituting interfaces.

Senior extension: At a public package boundary, both allowing inheritance and removing it later are compatibility decisions. If I support inheritance, I document and test the extension surface instead of leaving every protected detail accidental. If I prohibit it, I provide composition points or interfaces for expected variation. The question is which changes consumers are promised, not a universal final-versus-open rule.

PHP-OBJECTS-06 — Example answer

Solid response: I would establish which hook turns property syntax into work—often __get() on a proxy or Active Record model—and capture query traces to find the repeated access path. The serializer sees a property read, while the object may perform lazy loading. That hides I/O and makes response shape control query count.

I would make the owning query load the required data explicitly and serialize through a DTO/resource that only reads already-authorized, already-loaded values. In strict environments I may configure lazy loading to fail during development.

Senior extension: The design issue is not that all magic is bad; it is that an apparently local operation crossed persistence and authorization boundaries without an explicit contract. I would test query count and response shape together, and decide how optional relationships are represented. Replacing __get() with another wrapper is insufficient unless ownership of I/O becomes visible and measurable.

PHP-TYPES-02 — Example answer

Solid response: I would treat the decoded array as untrusted. First validate that required keys exist, distinguish missing from explicit null, validate the status against the accepted external vocabulary, parse the retry time into a date/time value, and verify that item IDs form a list of the expected scalar format. Then I would construct an immutable DTO or command with native property and constructor types. Inside the use case, code receives that object rather than repeatedly indexing an array.

An enum is suitable if status is a closed application-owned set. A nullable date represents an intentional absence only after parsing. A PHPDoc list<ItemId> or collection generic can tell static analysis the element type that native array cannot express.

Senior extension: I would decide how unknown future statuses behave instead of letting tryFrom() silently erase them. At an integration boundary, preserving the raw value for observability or mapping it to an explicit unknown result may be safer than throwing away the entire message. Runtime validation owns facts about external data; native types protect the constructed object; static analysis protects internal composition; contract tests check provider examples and failure policy.

PHP-TYPES-03 — Example answer

Solid response: Suppose an interface requires handle(CardPayment $payment): Receipt. An implementation may accept the broader Payment parameter because every CardPayment promised by callers is still accepted; that is contravariance. It may return the narrower DigitalReceipt because callers expecting a Receipt can still use it; that is covariance.

Narrowing the parameter would reject some inputs the interface promised. Broadening the return could give callers an object that does not satisfy the promised receipt contract. The allowed directions preserve substitutability.

Senior extension: I would avoid explaining variance as type conversion—no value is converted. It is a compatibility rule for inheritance and interface implementation. Mutable properties are usually invariant because reads want covariance while writes want contravariance. If a design requires elaborate variance reasoning across a deep hierarchy, I would also question whether smaller interfaces or composition would make the contract clearer.

PHP-TYPES-04 — Example answer

Solid response: A backed enum fits a small, closed set controlled by the codebase when each case has a stable string or integer representation, for example an internal order state. It prevents arbitrary strings and gives cases a place for small behavior. from() is appropriate when an unknown value is exceptional; tryFrom() supports an explicit recoverable parsing path.

I would not use an enum for administrator-created categories, provider-defined codes that evolve independently, or records needing labels, permissions, localization, or lifecycle in the database. A value object fits an open but validated scalar concept; an entity fits data with identity and persistence.

Senior extension: Adding an enum case is not automatically backward compatible. Exhaustive match expressions, serialized consumers, schemas, and older deployed workers may not recognize it. I would treat the backing value as a protocol and plan mixed-version deployment behavior. At external boundaries I may use a tolerant adapter even when the internal model is deliberately closed.

PHP-TYPES-05 — Example answer

Solid response: Readonly prevents a typed property from being reassigned after its permitted initialization. A readonly class applies that model to its instance properties and rejects dynamic properties. It is useful for stable messages and value objects.

The guarantee is shallow. If a readonly property contains a mutable DateTime or collection object, another method can mutate that same object even though the property still points to it. Readonly also does not define value equality or make methods side-effect free.

Senior extension: For a deep invariant I would depend on immutable collaborators such as DateTimeImmutable, copy mutable input at construction, and avoid exposing mutable internals. I would distinguish readonly from asymmetric visibility: restricted set visibility controls who can replace a property, while readonly limits replacement count. Neither automatically validates a state transition, so behavior methods remain better when writes require rules, events, or synchronization.

PHP-TYPES-06 — Example answer

Solid response: Native declarations express everything PHP can enforce directly: parameter, return, property, and constant types, plus unions, intersections, and enums. PHPDoc and static analysis add element types, generics, array shapes, non-empty strings, and similar development-time precision. Runtime validation establishes facts about external input before typed objects are created. Tests cover behavior and boundaries that types cannot prove.

For example, a repository can return list<User> in PHPDoc while its native return type is array; the analyzer checks consumers, but a database integration test still proves the mapping and query behavior.

Senior extension: I treat annotations as executable engineering policy only if CI runs the analyzer and the annotations match reality. A baseline is a migration tool, not evidence that old findings are harmless. Types cannot prove authorization, transaction isolation, remote schemas, or idempotency unless those guarantees are represented and checked elsewhere. I choose the narrowest useful contract while avoiding an annotation system so clever that ordinary maintainers cannot revise it safely.

PHP-ARRAYS-01 — Example answer

Solid response: PHP arrays are ordered maps. Valid decimal integer strings such as '8' become integer key 8, while a leading-zero string such as '08' remains a string key. Removing an item preserves the other keys, so an integer-keyed array may stop being a list. Insertion order remains significant.

The union operator keeps existing left-side keys and adds missing right-side keys. array_merge() overwrites later string keys and renumbers numeric keys. I choose based on collision semantics rather than treating both as concatenation.

Senior extension: I validate keys before insertion instead of depending on coercion, especially for external identifiers. List shape is a protocol concern because it affects JSON and static-analysis contracts. I use array_is_list() at defensive boundaries, array_values() only when deliberate reindexing is correct, and tests covering zero, numeric-looking strings, and gaps.

PHP-ERRORS-01 — Example answer

Solid response: Throwable is the common catchable interface. Exception is the normal base for application and library exceptions. Error represents engine-detected failures such as TypeError, ValueError, ArgumentCountError, and ParseError. Modern PHP lets code catch many conditions that older versions treated only as fatal errors.

Catchability only means control can enter a catch block. A violated type contract or partially completed mutation may leave no safe continuation. I catch where I can translate, compensate, add context, or terminate cleanly.

Senior extension: I distinguish expected domain alternatives from exceptional interruption. A declined payment may be a typed outcome, while a malformed provider response may be an exception. Broad Throwable catches belong at process/request isolation boundaries or around cleanup followed by rethrow; deep catch-and-continue code risks acknowledging incomplete work and hiding programmer errors.

PHP-ERRORS-02 — Example answer

Solid response: A custom error handler can turn selected warnings, notices, or deprecations into ErrorException, which makes ordinary exception flow and test failure possible. It should honor the chosen severity policy and return false when PHP’s built-in handling should continue.

It cannot convert every failure. Some startup, parse/compile, core, and process-level conditions occur outside the registered handler’s reach. Internal APIs also have their own documented behavior.

Senior extension: I usually make CI strict so warnings expose bad assumptions, while production reports non-fatal diagnostics without automatically turning every deprecation into an outage. I coordinate with the framework’s existing handler rather than installing competing global policy. I track deprecations explicitly, preserve suppression semantics only where unavoidable, and test handler failure paths with a minimal fallback logger.

PHP-ERRORS-03 — Example answer

Solid response: I translate at a boundary that understands both sides. A provider adapter can catch a transport exception and throw an application-owned unavailable or unknown-outcome exception with the original as previous. The HTTP, CLI, or queue boundary then maps that stable category to a response, exit code, or retry/failure record.

I do not expose raw messages to users. Logs retain the causal chain, safe request/provider identifiers, and correlation context.

Senior extension: Translation must not erase uncertainty. A timeout after sending a charge is not the same as a confirmed decline; the application may need a persisted pending state and reconciliation. I avoid logging the same throwable at every layer, which creates noise. One owning boundary reports it, while intermediate layers add structured context or rethrow with causality intact.

PHP-ERRORS-04 — Example answer

Solid response: A shutdown function can perform best-effort final observation when the script ends and can inspect error_get_last() for many traditional fatal errors. That can support minimal fatal telemetry or local cleanup.

It cannot resume the failed operation. Memory may be exhausted, execution time exceeded, initialization incomplete, or state already partly committed. Forced termination, host loss, or SIGKILL may skip PHP shutdown entirely.

Senior extension: I keep shutdown handling allocation-light and independent of remote services, possibly reserving a small memory buffer for fatal reporting. Correctness comes from transactions, idempotency, durable state, leases, and supervisor behavior—not a last callback. I test the SAPI and termination modes that matter because web, CLI, and worker shutdown behavior is operational context, not one universal guarantee.

PHP-ERRORS-05 — Example answer

Solid response: Destructor timing depends on reachability, reference counting, cycles, garbage collection, and shutdown. Abrupt process termination can bypass it, and shutdown ordering may make dependencies unavailable. Exceptions in cleanup can also obscure the original failure.

Therefore I do not charge, publish, acknowledge jobs, commit, or depend on destructor release of a distributed lock. I use explicit lifecycle methods and try/finally for locally owned cleanup.

Senior extension: Even explicit local cleanup cannot guarantee a remote effect; a lease expiry and fencing/ownership checks protect distributed work. For transactional effects I persist state and use recoverable workflows. Destructors may release a best-effort local wrapper, but the system must remain correct if they never run. Tests should include worker termination and repeated-process behavior, not only normal scope exit.

PHP-ERRORS-06 — Example answer

Solid response: A timeout means the result is unknown: the provider may have completed the operation and the response was lost. I record the attempt and idempotency key with local state, query/reconcile provider status, and retry only through provider-supported idempotent semantics. A database rollback cannot undo an external call.

I classify confirmed rejection, transient pre-send failure, and unknown post-send outcome separately because they require different actions.

Senior extension: I define the state machine before coding retries: pending, confirmed, declined, and needs-reconciliation, with unique constraints preventing duplicate active attempts. Retry policy considers the operation, exception stage, timeout budget, and provider contract—not merely exception class. Metrics track unknown outcomes and reconciliation age; operators need a safe replay path that reuses the same idempotency identity.

PHP-ARRAYS-02 — Example answer

Solid response: array_filter() preserves keys. If it removes index 1 from [0, 1, 2], the result has keys 0 and 2 and is no longer a PHP list. json_encode() represents that as a JSON object because a JSON array cannot express those keys. Reindexing with array_values() restores a list when list semantics are intended.

The repair belongs where the output schema is owned, not as a random encoding workaround. The producer should decide whether the field is a list or keyed object and normalize accordingly.

Senior extension: I add a contract test for the wire shape and enable JSON_THROW_ON_ERROR. I also review consumers because changing object to array may itself be a compatibility break if they learned the accidental format. For durable schemas, DTOs/resources make the normalization decision visible and prevent internal collection operations from silently changing the protocol.

PHP-ARRAYS-03 — Example answer

Solid response: Array assignment has value semantics, but PHP can share underlying storage until one logical value is mutated. Assignment may therefore add little memory. The first separating write allocates and copies the array structure so the original remains unchanged, creating a peak proportional to the data.

Nested objects are still shared handles, while nested arrays follow their own value/copy-on-write behavior. Adding references is not a safe performance fix because it changes aliasing semantics.

Senior extension: I reproduce the exact operation and measure memory before assignment and mutation, including peak usage and allocator behavior. I reduce large intermediates, stream when consumers can be incremental, or use a representation suited to the data. I do not promise exact byte savings because PHP build, entry types, and allocator retention differ; the production decision comes from a representative profile.

PHP-ARRAYS-04 — Example answer

Solid response: In foreach ($rows as &$row), $row aliases each element and remains an alias to the final element after the loop. If a later loop reuses $row by value, assignments can overwrite that last element repeatedly. unset($row) immediately after the reference loop breaks the alias.

That is explicit reference behavior. Copy-on-write separates array values on mutation, while object assignment shares an object handle. Neither is the same as aliasing a variable container with &.

Senior extension: I keep reference scopes tiny and prefer returning mapped values when memory and intent allow it. For legacy reference-heavy arrays I use a minimal executable probe because copies can preserve aliased elements in non-obvious ways. A coding rule to unset loop references is useful, but removing unnecessary reference mutation is the stronger design repair.

PHP-ARRAYS-05 — Example answer

Solid response: At identifiers, credentials, signatures, and sentinel-return boundaries, I avoid loose comparison because it coerces types and can collapse distinct values. I parse the expected external representation, then compare normalized values strictly. in_array() and array_search() should normally use strict mode; an array_search() result must be compared to false with !== because key 0 is valid.

Strict comparison before normalization can also be wrong: string '42' from HTTP and integer 42 may represent one valid ID after explicit parsing.

Senior extension: The contract decides normalization. Money uses integer minor units or a decimal value object, Unicode text may need product-specific normalization, and secrets need timing-safe comparison such as hash_equals(). I test adversarial zero-like inputs and migration behavior because PHP’s loose comparison rules have changed across major versions.

PHP-ARRAYS-06 — Example answer

Solid response: isset($data['x']) is false for both a missing key and a present null. array_key_exists() tests presence and therefore distinguishes those cases. $data['x'] ?? $default follows isset-like behavior, so null also selects the default. empty() additionally groups false, zero, '0', empty string, null, and missing values.

I choose the operation from the schema: required nullable fields need presence checking; optional fields may use a default; values such as zero often require explicit validation rather than empty().

Senior extension: I convert the raw array into a typed boundary object once, returning field-specific errors for missing, null, wrong type, and invalid value. That prevents every downstream layer from inventing different empty semantics. I retain raw presence information when PATCH-like operations distinguish “not supplied” from “set to null.”

PHP-RUNTIME-01 — Example answer

Solid response: PHP parses and compiles source into opcodes, then the Zend VM executes them. OPcache stores compiled script bytecode and related interned data in shared memory so later requests can skip much of loading, parsing, and compilation. It does not cache query results or application output.

If timestamp validation is disabled or delayed, changed files are not automatically reflected. A deployment must use immutable releases plus an explicit OPcache/process reload or invalidation strategy.

Senior extension: I monitor OPcache memory, key capacity, restarts, and hit behavior rather than only checking that the extension is enabled. Preloaded definitions have an even stronger process-start lifetime. I avoid mixed releases where workers see a blend of code and generated caches, and I coordinate FPM, queue, and Octane restart semantics because each runtime may retain a different layer of the old release.

PHP-RUNTIME-02 — Example answer

Solid response: A zval carries a PHP value and type metadata. Refcounted values can share allocations. Arrays and strings expose value semantics, so shared storage separates on a write when required. Object values contain handles, so assignment shares instance identity. Explicit references alias variable containers and are a separate feature.

Reference counting frees most unreachable values promptly. Cyclic graphs may need the cycle collector because their internal counts do not reach zero by themselves.

Senior extension: I use these concepts to explain observations, not depend on exact internal structure across PHP versions. Nested arrays and objects retain their own semantics, allocator retention means freeing values may not reduce RSS immediately, and references can alter separation. A focused probe and profile are safer than reasoning from folklore about “copies” or adding & for performance.

PHP-RUNTIME-03 — Example answer

Solid response: PHP’s memory counters cover allocator-tracked memory, while RSS also includes code and shared mappings, allocator arenas, native-extension allocations, JIT/OPcache mappings, and pages retained for reuse. Stable PHP usage with growing RSS can indicate fragmentation, native-library growth, or allocations outside ordinary tracking rather than a reachable PHP graph.

I record both metrics over many identical jobs, plus peak memory, workload size, extension use, and worker age.

Senior extension: I separate per-job peaks, reachable cross-job retention, and allocator/native behavior. I compare a minimal reproducer with extensions/features toggled, inspect heap/native profiles where available, and observe container kills and restart frequency. Recycling workers can cap impact, but I still need the slope and owning allocation to decide whether the repair is application lifecycle, extension upgrade, allocator tuning, or capacity policy.

PHP-RUNTIME-04 — Example answer

Solid response: pm.max_children caps concurrent FPM requests. I estimate a safe memory ceiling from measured worker RSS under representative load, subtract memory for the OS, OPcache, agents, and other services, then check whether the resulting concurrency meets latency and throughput needs. CPU and downstream capacity also constrain it.

When all workers are busy, requests queue. I inspect active/idle counts, listen queue, max-children events, request duration, CPU, RSS, database connections, and downstream saturation.

Senior extension: Stable execution time with rising queue time explains a good p50 but poor p99. Raising children may only move the bottleneck to the database and worsen contention. I load-test the whole dependency path, set admission/timeouts, and choose static/dynamic/ondemand behavior for traffic shape. pm.max_requests bounds damage from growth but is not the sizing formula or root fix.

PHP-RUNTIME-05 — Example answer

Solid response: FPM tears down ordinary request-scoped userland memory after each request even though the worker process is reused. Queue workers and Octane keep the booted application alive, so statics, singletons, listeners, closures, caches, loaded config/code, and accidentally captured request or tenant objects may survive.

I keep job/request data scoped, resolve current context at use time, unregister temporary callbacks, clear large graphs, and restart workers during deployments.

Senior extension: I test multiple different tenants/jobs in one process because ordinary feature tests often reboot the application and miss leakage. I instrument object/context identity and memory slope. Worker max-job/request limits and recycling are defense in depth; correctness must not rely on them. Graceful shutdown plus idempotent jobs protects deployments when a worker exits during real work.

PHP-RUNTIME-06 — Example answer

Solid response: JIT can help sustained CPU-bound PHP computation by compiling hot opcode paths to machine code. It does not reduce SQL, network, lock, filesystem, or queue delay. Most Laravel endpoints are dominated by those boundaries or framework/data work, so JIT may offer little.

I profile first, identify actual PHP CPU hotspots, then benchmark representative traffic with and without JIT, including warmup, memory, throughput, and p95/p99 latency.

Senior extension: I compare JIT against simpler changes: query plans, batching, serialization allocation, caching, OPcache health, or moving specialized computation to an optimized library. I include deployment failure and observability cost; explicitly enabled JIT initialization can fail startup on current PHP. I keep it only when repeatable production-like evidence beats that complexity.

PHP-COMPOSER-01 — Example answer

Solid response: composer.json supplies constraints; composer update resolves a currently satisfiable dependency graph and writes exact selections to composer.lock; composer install reproduces the committed lock when present. An application commits its lock so CI and production use the reviewed graph. Running update during deployment selects new allowed transitive versions and turns release into an unreviewed upgrade.

A library publishes constraints, because its own lock cannot control downstream consumers. It needs compatibility testing across supported ranges.

Senior extension: A lock does not freeze PHP, extensions, OS libraries, Composer, or external services. I build an immutable artifact in a controlled environment, run locked install, verify runtime platform requirements, and preserve provenance. For a library I may keep a contributor lock for tooling, but matrix tests and constraint design prove consumer compatibility rather than that one locked graph passes.

PHP-COMPOSER-02 — Example answer

Solid response: I read Composer’s conflict output and use why, why-not/prohibits, and targeted dry-run updates to identify the direct or transitive constraint blocking the target. I inspect root requirements, PHP/extensions, stability flags, repository overrides, and package release support. Then I update the smallest justified set with dependencies and review the lock diff.

Deleting the lock loses the known graph and may upgrade unrelated packages without explaining the incompatibility.

Senior extension: I decide whether the blocker requires upgrading/replacing a package, relaxing an unnecessarily narrow root constraint, contributing compatibility upstream, or staging the framework upgrade. I never lie with --ignore-platform-reqs as a production fix. CI runs tests/static analysis on the proposed graph and supported platforms, and I document temporary constraints with an owner and removal trigger.

PHP-COMPOSER-03 — Example answer

Solid response: PSR-4 maps a leading namespace prefix to base directories and maps remaining case-sensitive namespace and class segments to matching directories and a .php filename. The autoloader must fail quietly so stacked loaders can continue. It does not eagerly load files, verify the file declares the requested symbol, or enforce one class per file as an engine rule.

Case-insensitive local filesystems can hide filename or namespace case mismatches that fail on Linux.

Senior extension: I inspect the fully qualified name, Composer root/package mappings, generated autoload metadata, exact deployed path/case, and optimized mode. I keep side-effectful global code out of PSR-4 class files and use files autoload only deliberately. A production class-not-found after deploy can be stale generated metadata or authoritative classmap behavior, not just a missing source file.

PHP-COMPOSER-04 — Example answer

Solid response: Optimized autoloading builds a classmap for known PSR symbols, reducing filesystem work and fitting immutable production builds. Authoritative mode additionally says a symbol absent from the map does not exist, which is fast but breaks runtime-generated discoverable classes. APCu can cache successful and failed lookups without declaring the map authoritative, but requires APCu and lifecycle-aware cache use.

Development normally keeps flexible PSR-4 fallback so new classes appear without rebuilding everything.

Senior extension: I choose from measured lookup behavior and framework/package needs. Production builds regenerate metadata after source changes and test boot under the exact flags. I distinguish this from OPcache: Composer locates a defining file, while OPcache retains its compiled opcodes. Turning on every optimization without testing can produce production-only class-not-found failures.

PHP-COMPOSER-05 — Example answer

Solid response: Composer install/update can execute package code through plugins and scripts with the Composer process’s user, filesystem, network, and credentials. I pin/review the lock graph, restrict allowed plugins, review scripts and custom repositories, avoid root, and build with minimal credentials in isolation. I run audit/advisory and abandoned-package policy and verify platform requirements in the target runtime.

--ignore-platform-reqs and unreviewed repositories weaken compatibility and provenance. --no-plugins --no-scripts is useful for inspecting untrusted packages but may not produce a functional build.

Senior extension: I treat lock changes like source changes: direct/transitive code, distribution URL/reference, installer behavior, licenses, advisories, and ownership all need review proportional to risk. Audit feeds are incomplete, so I also need artifact provenance, dependency minimization, secret isolation, reproducible builds, monitoring, and fast patch capability. A compromised maintainer release can be semantically valid and advisory-free.

PHP-COMPOSER-06 — Example answer

Solid response: A PSR helps when packages genuinely need a stable interchange seam—for example a library accepting PSR-3 logging or PSR-18 HTTP clients without depending on one framework. Laravel can adapt its richer facilities to those contracts. Inside one application, wrapping a perfectly adequate framework API only to mention a PSR may add indirection without alternate consumers or implementations.

I choose the narrowest contract the boundary needs and keep framework-specific behavior where it belongs.

Senior extension: A PSR guarantees only its stated semantics. PSR-11 service lookup does not make service location good application design, and PSR-7 message immutability does not prove arbitrary streams are immutable or repeatable. I evaluate switching cost, lost framework capability, testing boundary, and ownership. Interoperability is valuable at package and infrastructure edges; architecture theater is an adapter with no credible second side.

PHP-MIGRATION-01 — Example answer

Solid response: I inventory every PHP runtime, SAPI, extension, INI policy, dependency/tool constraint, and long-running process. I collect deprecations and production baselines on the old version, read each intermediate migration guide, then upgrade blocking packages separately where compatibility allows. CI runs both the supported floor and target runtime before production changes.

I canary the target with comparable latency, errors, memory, jobs, and business outcomes, then expand exposure and retire old compatibility paths.

Senior extension: I model the compatibility graph rather than only PHP syntax: Laravel, Composer platform packages, extensions, agents, PHPUnit/static analysis, base images, serialized payloads, and deploy topology. I keep framework and architecture rewrites out of the runtime change unless required. Immutable artifacts, explicit worker restarts, reversible traffic routing, and expand/contract data changes make rollback or roll-forward real.

PHP-MIGRATION-02 — Example answer

Solid response: I enable full diagnostics in development/CI, exercise representative paths, group deprecations by application/vendor owner, and establish a baseline that fails newly introduced findings. The backlog should decline continuously, with framework/package upgrades used for vendor issues rather than editing vendor files.

I do not automatically throw every production deprecation as an exception. I report them to owned telemetry with rate controls so advisory information does not become an outage.

Senior extension: I prioritize deprecations by removal version, traffic criticality, semantic risk, and dependency lead time. Canaries compare deprecation and error rates. Suppressions require a reason, owner, and expiry. I read intermediate migration guides because severity and behavior changes may not emit a simple deprecation on the current branch. The success metric is an upgradeable codebase, not a hidden log.

PHP-MIGRATION-03 — Example answer

Solid response: I start with well-tested seams and new code, run static analysis to enumerate callers and observed values, and add explicit parsing at HTTP/database/serialization boundaries. Then I add native parameter and return types and typed properties incrementally. I test coercive inputs, nullable/missing distinctions, uninitialized properties, and inherited signature compatibility.

I keep PHPDoc where it adds generics or shapes and remove only redundant annotations.

Senior extension: Types can expose bad historical data and proxy/hydration assumptions, so I sample or validate persisted records before tightening properties. Sentinel-return replacement is a consumer-visible API migration, not cleanup. For packages I use compatibility policy and shims; for applications I deploy adapters first, then tighten internals. Static-analysis baselines ratchet down alongside runtime assertions and integration tests.

PHP-MIGRATION-04 — Example answer

Solid response: For accidental dynamic properties I declare the real property and type it. For genuinely open record data I use an explicit internal map or carefully designed magic access; for metadata on foreign objects I use WeakMap. AllowDynamicProperties can temporarily unblock a third-party or broad legacy hierarchy, but it preserves the weak contract and needs a removal plan.

For annotations, I confirm the framework supports attributes, choose one metadata owner, migrate vertical slices, and test discovery/cache behavior.

Senior extension: Hydrators and serializers may depend on undeclared field injection, so I characterize real payloads before changing it. Running annotations and attributes together can register routes or mapping twice, so dual-read needs precedence and telemetry. I distinguish descriptive docblocks from machine metadata; only the latter belongs in attributes. The target is explicit ownership, not syntax replacement for its own sake.

PHP-MIGRATION-05 — Example answer

Solid response: I upgrade the test runner/configuration so it supports the target PHP and reports deprecations, then repair removed APIs and listeners. PHPUnit class tests and Pest can coexist; I migrate syntax only when it improves readability or maintenance. In parallel I introduce PHPStan or Psalm at a sustainable level, baseline existing findings, block new ones, and shrink the baseline.

Tool migration is successful only if behavioral confidence remains or improves.

Senior extension: I preserve and add integration, serialization, real-driver, worker-lifetime, and concurrency tests around migration risks. Static analysis cannot prove runtime extensions, database isolation, or external contracts. I avoid one branch that changes PHP, framework, test syntax, mocks, and architecture simultaneously. CI matrices test supported runtime/dependency ranges, while flaky and mutation/coverage evidence guides where confidence is actually weak.

PHP-MIGRATION-06 — Example answer

Solid response: During rolling deployment, old and new web or queue workers may share databases, caches, messages, and files. New code can write an enum value, required field, serialized object shape, or job payload the old code cannot read. Schema changes can also make rollback code incompatible.

I use expand/contract migrations, additive/versioned payloads, tolerant readers, stable scalar queue data, and restart long-running workers deliberately.

Senior extension: I specify the compatibility window and test both directions: old writer/new reader and new writer/old reader. Feature activation waits until all consumers understand the new shape. Destructive cleanup occurs only after telemetry proves old versions are gone and queues drained. Rollback artifacts and data compatibility are tested; if external effects or migrations are irreversible, I plan roll-forward and reconciliation instead of promising impossible rollback.