PHP interview bank

Questions interviewers actually ask, each answered at the length you would say it aloud, with the topic to reread if you were not sure.

47 questions Junior Senior
All levels Junior Mid Senior
Reveal one by one Show all answers
Report an error

Language core

25 questions
01 How do array_map() and array_filter() differ in their treatment of keys? Mid common reveal ▾ hide ▴

array_filter always preserves the input keys, so filtering an indexed array can leave gaps. That matters when the result is encoded as JSON because a non-list PHP array becomes a JSON object. array_map preserves keys only when it receives exactly one input array. With two or more arrays, it aligns values by iteration position and returns sequential integer keys. I keep business keys through processing and call array_values only at a boundary that explicitly requires a list. If a map callback needs keys, I use a clear foreach or pass keys and values deliberately.

Was this clear?
02 Why should array_reduce() usually receive an explicit initial value? Mid common reveal ▾ hide ▴

The initial value defines the accumulator before the first item and becomes the result for empty input. In PHP, omitting it means the first callback receives null; PHP does not adopt the first array element the way JavaScript reduce does. An explicit identity also stabilizes the result type: 0 for addition, 1 for multiplication, an empty string for concatenation, or an empty array for grouping. If no meaningful empty result exists, I validate input and throw a domain exception instead of allowing an accidental null to flow through the callback.

Was this clear?
03 When would you use array_merge() instead of the array union operator? Mid common reveal ▾ hide ▴

I use array_merge when later string-keyed configuration should override earlier values or when integer-keyed lists should be concatenated and renumbered. The union operator keeps the left value for every duplicate key and preserves integer keys, so it fits left-biased defaults or maps whose numeric identifiers matter. Neither operation is a universal deep merge. array_merge is shallow, while array_merge_recursive can turn duplicate scalar values into arrays. Before choosing, I state the conflict policy for string keys, integer keys, and nested arrays, then write one test for each case.

Was this clear?
04 What contract must a usort() comparator satisfy, and what else does a caller need to know? Mid occasional reveal ▾ hide ▴

The comparator returns a negative integer when the left value belongs first, zero when the values are equal for sorting, and a positive integer when the right value belongs first. A spaceship expression usually states that contract cleanly; returning a Boolean cannot represent all three cases. usort modifies the array by reference and replaces all keys with sequential integers, so I copy first if the original order matters or choose uasort when associations must survive. On PHP 8 and later, equal elements retain input order, but deterministic output still needs an explicit tie-breaker when input order is uncertain.

Was this clear?
05 How can one PHP array represent both a list and a map? Junior common reveal ▾ hide ▴

A PHP array is an ordered map whose keys are integers or strings. It has list shape only when its keys are consecutive integers from zero in iteration order; array_is_list checks exactly that rule. String keys, a nonzero starting key, or a gap make it a map-shaped array, although its runtime type remains array. The distinction matters at boundaries: json_encode turns lists into JSON arrays and other PHP arrays into JSON objects. I document return shape and reindex with array_values only when keys have no business meaning.

read more Arrays
Was this clear?
06 When should you use array_key_exists() instead of isset()? Junior common reveal ▾ hide ▴

I use array_key_exists when the domain distinguishes a missing key from a key explicitly set to null. It tests only key presence, so both a scalar value and null count as present. isset asks whether the key exists and its value is not null, while the null-coalescing operator follows the same practical distinction. For a patch request, null might mean clear the field and absence might mean leave it unchanged, so array_key_exists is essential. When both states mean use a default, isset or ?? is shorter and accurately states the policy.

read more Arrays
Was this clear?
07 Why can unset() change the JSON shape of a PHP array? Mid common reveal ▾ hide ▴

unset removes one key-value pair but does not renumber the remaining integer keys. Deleting the middle of a list therefore leaves a gap, and a later unkeyed append normally continues with the next integer key rather than filling that gap. The value is still a PHP array, but array_is_list now returns false. json_encode represents it as a JSON object because JSON arrays cannot carry those explicit, nonconsecutive keys. If positions are the only meaning, I call array_values at the serialization boundary; if keys are identifiers, I preserve them deliberately.

read more Arrays
Was this clear?
08 What does copying a PHP array copy, and when can changes still be shared? Mid occasional reveal ▾ hide ▴

Ordinary assignment gives arrays value semantics: changing keys, scalar elements, or nested arrays in the copy does not change the original. PHP may defer physical duplication through copy-on-write, but that is an implementation optimization rather than aliasing visible to application code. Objects stored as elements are different because the copied arrays retain handles to the same objects unless those objects are cloned. An explicit reference assignment with & also creates shared mutation. I test container independence and object identity separately, and I unset a by-reference foreach variable immediately after its loop.

read more Arrays
Was this clear?
09 How do PHP runtime value types differ from declaration types? Junior common reveal ▾ hide ▴

A runtime type belongs to the current value, so one variable can hold an int and later a string. Declarations constrain selected positions such as parameters, returns, properties, and class constants. The sets overlap but are not identical. resource is a runtime type that userland declarations cannot name, while iterable describes either an array or a Traversable object rather than a distinct runtime value type. mixed, void, and never also express declaration contracts. I use get_debug_type for diagnostics and is_* functions for deliberate narrowing, not string comparisons against gettype output.

read more Data types
Was this clear?
10 What does declare(strict_types=1) enforce, and what does it leave untouched? Mid common reveal ▾ hide ▴

It is a file-level rule for scalar declarations. For user-defined function arguments, the relevant setting is in the file that makes the call, not merely the file that defines the function. Strict mode rejects scalar mismatches except that an int may satisfy a float parameter; it also tightens declared return values. It does not parse query strings, validate ranges, inspect array elements, or turn PHP into a statically typed language. I still parse untrusted input at a boundary, then pass narrow, validated values into strictly declared domain functions.

read more Data types
Was this clear?
11 Why is casting external input different from validating it? Junior common reveal ▾ hide ▴

A cast guarantees a result type by applying PHP conversion rules; it does not prove that the source representation was complete or meaningful. An expression such as (int) ($input[“quantity”] ?? null) can merge a missing field, malformed text, and a valid zero into the same integer. Validation first defines accepted raw types, syntax, range, and missing-value policy. Conversion happens only after those checks. That preserves useful errors and prevents authorization or pricing logic from acting on a plausible value manufactured from invalid input.

read more Data types
Was this clear?
12 What does an array type declaration guarantee about its contents? Mid common reveal ▾ hide ▴

Only that the outer value is an array. Native PHP does not encode list versus map shape, required keys, or element types in an array declaration. PHPDoc can describe forms such as array<string, int> or a named array shape, and a static analyzer can check cooperating code, but those annotations do not validate request data at runtime. At an external boundary I validate keys and values explicitly. For stable domain records, I usually map the validated data into a class with typed properties instead of passing an anonymous array through the application.

read more Data types
Was this clear?
13 When would you choose a pure enum instead of a backed enum? Junior common reveal ▾ hide ▴

I choose a pure enum when the cases only need to be distinct inside PHP and no stable scalar representation belongs to the domain. Every case is still a typed singleton with a name, methods, interfaces, and cases(). I choose a backed enum when values must round-trip through a database, JSON API, queue message, or configuration file. The string or integer value then becomes an external contract. I do not add backing values merely for labels or ordering; presentation text and domain order deserve explicit mappings.

read more Enums
Was this clear?
14 How do from() and tryFrom() differ, and where should each be used? Mid common reveal ▾ hide ▴

Both methods belong to backed enums and look up the backing value, not the case name. from() returns the case or throws ValueError, so it fits an internal invariant such as a database column that is constrained to legal values. tryFrom() returns the case or null, so it fits customer-controlled input where the application must produce a validation response. I still verify the raw type before either call. I also keep missing input separate from an unknown value instead of collapsing both into a default case.

read more Enums
Was this clear?
15 Does matching on a PHP enum give compile-time exhaustiveness checking? Mid common reveal ▾ hide ▴

PHP itself does not prove an enum match exhaustive at compile time. If no arm matches and there is no default, execution throws UnhandledMatchError. That runtime failure is useful because adding a case does not silently reuse an unrelated policy, but it appears only when the path runs. I omit default for decisions that require an explicit result per case, run a parameterized test over cases(), and enable a static analyzer that can report omissions earlier. A default remains appropriate only when every future case may safely share it.

read more Enums
Was this clear?
16 What can break when an enum case or backing value changes? Senior occasional reveal ▾ hide ▴

A backed value may already live in database rows, JSON payloads, messages, cache keys, and fixtures. PHP serialization depends on the enum type and case name instead, so renaming either identifier breaks a different set of data. Adding a case can also break older consumers: tryFrom returns null for the new value, while an uncovered match fails only at runtime. I treat published values as protocol fields. Consumers learn both representations or an explicit unknown-value policy first, stored data is migrated next, and producers stop writing the old form last.

read more Enums
Was this clear?
17 How do PHP diagnostics differ from Throwable objects? Mid common reveal ▾ hide ▴

PHP diagnostics carry an E_* integer level, message, file, and line. Eligible levels can reach set_error_handler(), whose return value decides whether normal PHP display or logging continues. Throwable is the object hierarchy shared by Exception and Error; it propagates through try/catch and can reach set_exception_handler(). Error is therefore not the same thing as E_ERROR. I classify the failure source first, because an E_ALL mask does not make an error handler catch TypeError, and a catch block does not receive ordinary E_USER_WARNING diagnostics unless code explicitly converts them to ErrorException.

Was this clear?
21 Why can catch (Exception) miss a PHP failure, and when would you catch Throwable? Mid common reveal ▾ hide ▴

Throwable is the common interface above the Exception and Error branches. An application RuntimeException belongs to Exception, while engine failures such as TypeError belong to Error, so catch (Exception) misses the latter. I keep local catches narrow and name the failures they can actually recover from. I catch Throwable at a transaction, request, job, or CLI boundary when every escaping failure requires the same rollback and termination policy. I do not mechanically replace every Exception catch with Throwable, because that can turn a programming defect into a misleading fallback. Tests send one object from each branch through the boundary.

read more Exceptions
Was this clear?
25 What is the difference between an expression and a statement in PHP, and why does it matter during review? Junior common reveal ▾ hide ▴

An expression is evaluated and produces a value; a statement is a complete execution unit such as an assignment statement, return, or function call. The distinction matters because PHP permits side effects inside expressions. Assignment produces the assigned value, so an accidental single equals sign inside if is valid code rather than a syntax error. I keep state changes in separate statements unless a compact form has a clear contract. During review, I expand mixed assignment, comparison, and logic with parentheses, check the resulting type, and test both branches.

Was this clear?
26 How do you choose between if, match, and switch in modern PHP? Junior common reveal ▾ hide ▴

I use if for ordered predicates, especially when each branch asks a different question or performs statements. I use match when one subject should produce exactly one value: it compares strictly, returns the selected arm, and never falls through. I keep switch mainly for existing code or deliberate statement-oriented fall-through, remembering that case matching is loose and nonempty cases usually need break. The choice is not cosmetic. I test overlapping predicates in priority order, numeric strings against integers when migrating from switch to match, and the unmatched path when no default arm exists.

Was this clear?
29 How does PHP resolve unqualified, qualified, and fully qualified names inside a namespace? Mid common reveal ▾ hide ▴

I separate static class-like names from functions, constants, and dynamic strings. An unqualified class name first uses a file-local import alias and otherwise belongs to the current namespace. A qualified name can replace an imported first segment; otherwise it is relative to the current namespace. A leading backslash makes a name fully qualified from the global root, while namespace\ is explicitly relative. Unqualified functions and constants may fall back globally when no namespaced definition exists, but classes do not. Dynamic strings bypass these static import rules, so I pass Type::class when a complete runtime name is needed.

read more Namespaces
Was this clear?
30 What is the difference between a namespace import and autoloading in PHP? Junior common reveal ▾ hide ▴

A use declaration is compile-time, file-local notation. It tells PHP how a short class, function, or constant name expands, but it does not include a file, install a package, or define the target. After PHP resolves a class-like reference, a registered autoloader may receive that complete name and locate its definition. Composer commonly implements that second step with PSR-4. I debug the phases in order: first print or inspect the exact resolved class name, then derive the expected path from composer.json, regenerate autoload metadata if configuration changed, and exercise the class through vendor/autoload.php.

read more Namespaces
Was this clear?
33 What does a PHP union type guarantee, and what validation still belongs at an application boundary? Mid common reveal ▾ hide ▴

A union such as int|string guarantees only that the runtime value satisfies at least one declared member under PHP’s call-site typing rules. It does not prove that an integer is positive, a string is a known status, or an array has required keys and element types. I validate shape and domain constraints where HTTP, CLI, database, or message data enters, then normalize it into a narrower internal form. I test every accepted union member, look-alike values such as 1 and “1”, boundary values, and types that must be rejected.

Was this clear?
37 When do __get() and __call() run, and what do they not intercept? Mid common reveal ▾ hide ▴

__get runs when code reads an undefined property or a declared property that is inaccessible from the current calling scope. __call is the corresponding fallback for an inaccessible instance method call. Neither hook wraps ordinary access to a public declared member, so I would not put authorization or auditing there if every access must pass through it. I test through real external syntax rather than invoking the hook directly, and include public, private, missing, and misspelled members. The same visibility-sensitive reasoning applies to __set, __isset, __unset, and __callStatic.

read more Magic methods
Was this clear?
46 How does PHP resolve method precedence when a class uses traits? Mid common reveal ▾ hide ▴

A method declared in the current class wins over an imported trait method, and a trait method wins over an inherited parent method. Two used traits that contribute the same method cause a fatal error unless the use block selects one with insteadof. I can retain the excluded implementation with as and optionally give that alias different visibility. The alias is additive: it does not rename or remove the original method. When reviewing a change, I build a member-source table for the class, parent, and nested traits, then test every final public entry point after adaptation.

read more Traits
Was this clear?

Runtime behavior

5 questions
18 What contract should a set_error_handler() callback follow? Mid common reveal ▾ hide ▴

The callback receives severity, message, file, and line for diagnostics eligible under its registration mask. Before converting a diagnostic, it should test error_reporting() & $severity so suppression and the current policy remain meaningful. Returning false delegates to PHP default handling; returning true says the custom path owns display and logging completely. I keep the callback small, avoid dependencies that can fail recursively, and pair temporary registration with restore_error_handler() in finally. In a long-lived worker or Fiber-based application, I also verify that process-global handler state cannot leak across requests or interleaved tasks.

Was this clear?
20 What can a PHP shutdown handler safely do after a fatal termination? Senior occasional reveal ▾ hide ▴

A shutdown function runs after normal completion and on many termination paths, so error_get_last() must be checked for null and filtered against an explicit fatal-level allowlist. It can record a small, idempotent event or release simple process resources, but it is not a recovery boundary. Transactions may be incomplete, memory may be exhausted, and output may already have started. I do not continue domain writes, replay payments, or print one universal HTML page into JSON and streaming responses. Retry decisions belong to an external worker or caller that can inspect durable state.

Was this clear?
23 What does finally guarantee, and what should not be placed inside it? Mid common reveal ▾ hide ▴

A finally block runs as control leaves its try/catch structure through normal completion, return, a handled exception, or continuing propagation. I use it for bounded cleanup owned by that scope: releasing a lock, closing a stream, rolling back temporary state, or restoring a handler. I do not return from finally because that replaces a pending return value and can suppress an exception. I also avoid large cleanup paths that may fail unpredictably. Acquisition and release stay close together, and tests force failure after acquisition to prove cleanup runs without changing the original result or exception.

read more Exceptions
Was this clear?
27 Why is operator precedence not the same as evaluation order in PHP? Mid occasional reveal ▾ hide ▴

Precedence tells the parser how operators and operands group, while associativity resolves nesting between operators at the same precedence. Neither generally promises which subexpression executes first. That matters when calls, increments, assignments, or array updates share one expression: an observed order on one runtime is not a portable contract. I split required side effects into separate named statements. I rely on ordering only where the operator defines conditional evaluation, such as the right side of &&, ||, ??, or the selected branch of a conditional expression, and I still avoid hiding mandatory writes there.

Was this clear?
35 When does a PHP attribute affect program behavior, and when are its errors detected? Mid common reveal ▾ hide ▴

An attribute is metadata, so writing #[RequiresRole] does not enforce authorization by itself. A consumer must locate it through reflection, decide how repeated entries combine, instantiate it when needed, and apply the policy. getAttributes can return descriptors without running constructors. ReflectionAttribute::newInstance performs instantiation and can expose invalid targets, missing arguments, or a class that is not declared as an attribute. I run a startup or integration test through the real consumer, including absent metadata, repeats, bad arguments, bad targets, and both allowed and denied authorization outcomes.

Was this clear?

Production operations

1 question
19 How would you configure and test PHP error reporting in production? Mid common reveal ▾ hide ▴

I keep error_reporting at E_ALL so warnings, notices, and deprecations remain observable, set display_errors off so internal detail cannot enter responses, and keep log_errors on with an access-controlled destination. Deployment configuration, not request code alone, owns startup settings. Logs use stable event codes and correlation IDs with an explicit field allowlist; passwords, tokens, cookies, authorization headers, and arbitrary request bodies are excluded. Tests assert effective configuration, sufficient internal context, and the absence of paths, stacks, SQL, and secrets from public responses. I also test failure of the logging destination itself.

Was this clear?

Application design

5 questions
22 How do you translate an exception across abstraction layers without losing its cause? Mid common reveal ▾ hide ▴

I catch the low-level type at the layer that understands it, add operation context, and pass the original Throwable as the third constructor argument of the new exception. The upper layer then depends on a stable domain type while getPrevious() retains the driver or service cause. The new message describes this layer’s failed operation rather than copying the old message. I normally log the chain once at the outer boundary that owns correlation and redaction context. Logging and rethrowing at every layer creates duplicate events, while omitting $previous leaves only prose and destroys the programmatic cause relationship.

read more Exceptions
Was this clear?
24 How would you design and test a public exception contract in PHP? Senior occasional reveal ▾ hide ▴

PHP exceptions are unchecked, so signatures do not force callers to acknowledge them. I document the stable application types a public method may throw, choose types by caller policy rather than message wording, and keep infrastructure classes behind a translation boundary. Tests cover success, every documented failure type, the previous-exception link, and cleanup on each exit path. I do not make clients parse getMessage() or treat Exception::$code as an automatic HTTP status. If callers need structured decisions, the exception exposes a narrow read-only property or subtype, while the public response uses a separate stable error code.

read more Exceptions
Was this clear?
28 How would you keep a PHP business function independent of its web or CLI entry point? Mid common reveal ▾ hide ▴

I let the entry point own protocol details. A web adapter reads selected superglobal fields; a CLI adapter reads argv and chooses exit codes. Each adapter validates and normalizes raw strings before passing ordinary typed values to a business function. The function receives dependencies and data as parameters, returns a stable domain result, and does not read globals, print responses, or terminate the process. Tests then call it without constructing a request or spawning a command. At the outer boundary, the adapter converts domain failures into an HTTP response, stderr message, or nonzero exit code.

Was this clear?
31 How would you safely build a factory that selects namespaced classes from an external key? Senior occasional reveal ▾ hide ▴

I do not concatenate the request value into a class string because imports do not rewrite dynamic strings and autoloading an attacker-influenced name can trigger unintended code. I define a closed map from protocol keys to class constants, such as csv to CsvExporter::class, reject unknown keys before calling class_exists or new, and type the return as a shared interface. Construction may still have side effects, so the allowlist check comes first. Tests cover every accepted key, case variants, separators, empty input, and an unknown key, and they assert that each mapped class loads through Composer and implements the interface.

read more Namespaces
Was this clear?
38 How would you design a safe __call() proxy? Senior common reveal ▾ hide ▴

I start with a closed map from public dynamic names to fixed operations. For each entry I validate argument count, runtime types, authorization, and the expected return type before invoking a narrow service interface. Unknown names always raise BadMethodCallException. I do not use method_exists as authorization and do not forward a caller-controlled name directly to an internal service, because a newly public service method would silently widen the proxy. If the name set is stable and small, I skip __call entirely and declare ordinary typed methods; that improves discovery, static analysis, refactoring, and tests.

read more Magic methods
Was this clear?

Tooling

1 question
32 A class loads on a developer laptop but fails in Linux CI. How do you investigate its namespace and PSR-4 mapping? Mid common reveal ▾ hide ▴

I start with the complete class name requested at the failing call, not its local alias. For the matching composer.json prefix, I remove that prefix, convert the remaining namespace separators to directories, and append .php to the class name. I compare every directory, filename, declaration, and reference with exact case because a case-insensitive filesystem can hide drift locally. I also check singular versus plural segments, confirm the entry point loaded the intended vendor/autoload.php, and run composer dump-autoload after configuration changes. Finally, I reproduce through the real autoloader rather than merely linting the target file.

read more Namespaces
Was this clear?

API design

1 question
34 How do named arguments change API compatibility and array unpacking in PHP 8? Mid common reveal ▾ hide ▴

A named call binds the parameter’s textual name, so renaming a public parameter can break callers even when its position and type stay unchanged. Positional arguments must come first, and an unknown or duplicate named binding raises Error. String keys from an unpacked array are treated as argument names, which means forwarding an untrusted payload with …$payload exposes the call directly to unknown keys and type failures. I project external data through an allowlist before unpacking, keep public parameter names stable, and retain integration tests that call important APIs by name.

Was this clear?

Migration

1 question
36 How would you safely migrate switch statements and nested null checks to PHP 8 syntax? Senior occasional reveal ▾ hide ▴

I first characterize old behavior with values of different types, every case, fall-through paths, and the default. match uses strict comparison, returns one expression, and throws UnhandledMatchError when no arm matches, so a mechanical switch replacement can change all three behaviors. For nested null checks, I identify every independently nullable hop and the domain meaning of each absence. Each such hop needs ?->, while ?? supplies a separate fallback policy. I also move required auditing, authorization, and other side effects out of method arguments that a nullsafe chain may skip.

Was this clear?

Object lifecycle

2 questions
39 What exactly happens when a PHP object is cloned? Mid common reveal ▾ hide ▴

PHP first makes a shallow copy of the object properties, then invokes __clone on the new object. Object-valued properties therefore still reference the same nested instances unless __clone replaces them. I classify properties by ownership before writing the hook: owned value objects may be cloned, services may remain shared or be reinjected, identities may prohibit cloning, and tokens or caches may need clearing. Tests mutate every nested mutable category and assert both isolation and intended sharing. Arrays deserve special attention because copying an array does not clone objects stored inside it.

read more Magic methods
Was this clear?
44 What is the difference between assigning, referencing, and cloning a PHP object? Mid common reveal ▾ hide ▴

Ordinary object assignment copies an object identifier, so both variables locate the same instance and mutations are visible through either name. It does not make the variables themselves aliases: reassigning one variable does not reassign the other. An explicit ampersand creates that variable-reference relationship and is rarely needed merely to share an object. clone creates a new top-level identity, then shallow-copies properties. Nested object properties therefore remain shared unless __clone replaces them. I test scalar, array, and nested-object mutations separately and use === only when the contract requires the exact same runtime instance.

Was this clear?

Persistence and security

1 question
40 What contract should __serialize() and __unserialize() establish? Senior occasional reveal ▾ hide ▴

__serialize returns a deliberately selected, versioned state array rather than every private field. It excludes tokens, connections, closures, request objects, and caches. __unserialize validates the version, required keys, nested types, and domain invariants, then initializes every typed property because normal construction is not the restoration path. I keep fixed old payloads as migration tests, not just fresh round trips. PHP serialization is an object protocol with autoloading and hook behavior, so I never pass attacker-controlled bytes to unserialize. External input uses a data-only format and explicit validation; trusted stored bytes still need integrity protection.

read more Magic methods
Was this clear?

Object design

3 questions
41 How does encapsulation help a PHP class maintain an invariant? Mid common reveal ▾ hide ▴

Encapsulation is more than changing fields to private. I define the states that must always hold, require all essential values in the constructor, reject invalid combinations there, and expose only operations that preserve those rules. Typed properties prevent some invalid assignments but do not express ranges, relationships, or lifecycle order. I also avoid returning mutable internal objects when callers could use them to bypass validation. Tests construct invalid inputs, run every public mutation, and assert the invariant through observable behavior rather than reading private fields with reflection.

Was this clear?
42 When would you choose an interface, an abstract class, or composition in PHP? Mid common reveal ▾ hide ▴

I start from the caller. If it needs only a capability that unrelated implementations can provide, I define a small interface. An abstract class fits implementations that genuinely share state, lifecycle, and a stable algorithm skeleton, because PHP allows only one parent class. If the goal is merely to reuse work or vary one policy, I put that behavior in a collaborator and compose it through the constructor. A class can implement several interfaces while still extending one parent. I avoid abstractions justified only by hypothetical future implementations and add them when a real substitution boundary exists.

Was this clear?
45 How do a PHP trait and an interface differ, and when would you use both? Mid common reveal ▾ hide ▴

A trait reuses implementation by contributing members to each consuming class; it is not an instantiable object or a substitutable capability type. An interface defines the public operations that callers may require from unrelated implementations, but in PHP 8.3 it does not supply method bodies. I use both when several classes promise the same interface and genuinely share one implementation: each class implements the interface and uses the trait. I put host requirements in abstract trait methods. If the behavior needs a replaceable service or independent lifecycle, I inject a collaborator instead of hiding that dependency in a trait.

read more Traits
Was this clear?

Inheritance

1 question
43 How do you review whether a PHP subclass safely substitutes for its parent? Senior occasional reveal ▾ hide ▴

I check more than signature compatibility. The child must accept every input the parent contract accepts, preserve promised return meaning, maintain inherited invariants, and not introduce surprising required side effects or failures. Its constructor must establish all parent state, usually through parent::__construct. I run the parent contract tests against each child through the parent type rather than calling only child-specific methods. PHP can check compatible parameter contravariance and return covariance, but it cannot prove business preconditions, exception policy, or state transitions. If substitution needs caveats, composition or a narrower interface is usually clearer.

Was this clear?

Object state

1 question
47 What state and inheritance risks do trait properties introduce in PHP 8.3? Senior occasional reveal ▾ hide ▴

An instance property declared by a trait becomes state on every consuming object and shares the class property namespace, so same-named declarations must be structurally compatible. A static trait property is class-level state, not per-object state, and can leak across requests or tests in a persistent process. In PHP 8.3, a child that only inherits the trait-backed member shares inherited storage, while a child that uses the same trait again gets distinct storage from its parent. I test two objects, two unrelated classes, an inheritance-only child, and a trait-reusing child, and I avoid static storage for tenant-owned data.

read more Traits
Was this clear?