PHP Interview Questions

Commonly asked PHP interview questions on == vs ===, traits vs interfaces, and PSR-4 autoloading.

A curated set of PHP interview questions, ordered roughly from fundamentals to more advanced — the kind you'll actually be asked in real screens and on-sites.

Language fundamentals

Q: What's the difference between == and ===? == ("loose equality") compares values after converting them to a common type if they differ, which produces well-known surprising results like 0 == "abc" being false in PHP 8 but historically true in PHP 7 and earlier, or "1" == "01" being true. === ("strict equality") requires both the value and the type to match with no conversion at all, and is almost always the right default to reach for unless you have a specific reason to want type coercion.

Q: What's the difference between isset(), empty(), and is_null()? isset($var) returns true if the variable exists and is not null. empty($var) returns true if the variable doesn't exist, or holds a "falsy" value (null, false, 0, "", "0", an empty array) — it's a broader, looser check than isset. is_null($var) requires the variable to already exist (an undefined variable triggers a warning) and checks specifically whether its value is null, with no other falsy values counted.

Q: What's the difference between match and switch? match uses strict (===) comparison, has no fallthrough between arms (so no break needed), and is itself an expression that returns a value. switch uses loose (==) comparison by default, requires explicit break statements to avoid falling through to subsequent cases, and is a statement rather than an expression. match is generally the safer, more modern choice when every case maps directly to a single result.

OOP

Q: What's the difference between a trait and an interface? An interface is a pure contract — method signatures with no implementation — and a class can implement many of them. A trait provides actual reusable implementation that gets copied into any class that uses it, working around PHP's lack of multiple class inheritance. In short: an interface says "you must implement this," a trait says "here's a ready-made implementation you can borrow."

Q: How does PSR-4 autoloading work, and why does it matter? PSR-4 defines a standard mapping from namespace prefixes to directory paths (declared in composer.json), so a class's fully-qualified name deterministically maps to its file location — App\Services\InvoiceGenerator must live at app/Services/InvoiceGenerator.php. This means Composer's generated autoloader can locate and load any class on first use with zero manual require statements and no per-class registration, which is exactly why a real application like a Laravel project never needs to require its own model or controller files by hand.

Practical

Q: When would you reach for a readonly property or a backed enum instead of a plain public property or string/int constant? readonly properties enforce, at the language level, that a value object (money, a date, an ID) can never be mutated after construction — turning what used to be a "please don't mutate this" convention into something the engine actually rejects. Backed enums replace loosely-typed string/int constants scattered through a codebase ('admin', 'editor') with a genuine, closed type that IDEs can autocomplete, match can exhaustively check, and the engine validates when reconstructing one from a stored value via ::from().

Error handling

Q: What does a finally block guarantee, and what's the practical difference from putting cleanup code after the try/catch? finally runs regardless of how the try block ends — normal completion, an exception caught locally, or an exception that propagates past every catch uncaught — even firing during stack unwinding before an uncaught exception continues outward past the current function. Code placed after a try/catch (with no finally) would simply be skipped entirely if an exception propagated out uncaught, so finally is the only place guaranteed to run cleanup (closing a resource, logging an attempt) under every possible outcome.

Q: Why build a custom exception hierarchy (e.g., a base PaymentException with specific subclasses) instead of throwing PHP's built-in Exception everywhere? A hierarchy lets calling code catch at whatever level of specificity it actually needs — a broad catch (PaymentException $e) handles anything payment-related uniformly, while a narrower catch (CardDeclinedException $e) reacts specifically to that one case, without either call site needing to know about the other's concerns. Throwing only the generic built-in Exception everywhere would force every catch site to inspect a message string to figure out what actually went wrong, which is fragile and gives the type checker nothing to verify.

Composer and packages

Q: What's the practical difference between the ^ and ~ version constraint operators in composer.json? ^2.4.1 allows any version semver considers backwards-compatible, effectively >=2.4.1 <3.0.0, picking up new minor features and patches automatically. ~2.4.1 is narrower, >=2.4.1 <2.5.0, allowing only patch-level updates within the same minor version. ^ is the default choice in practice; ~ is used when you deliberately want to accept only bug fixes, not new features, from a dependency.

Q: Why does an application typically commit its composer.lock file, while a library being published for others to depend on typically does not? composer.lock pins the exact resolved version of every dependency actually installed, making composer install fully reproducible across every machine and deployment — essential for an application, where you want every environment running identical code. A library, however, is meant to be installed inside other projects with their own separate dependency trees; locking its own dependencies to one exact set would work against a consuming project's ability to resolve one compatible set of versions across everything it depends on together.

Testing

Q: What's the difference between assertSame and assertEquals in PHPUnit, and which should be the default? assertSame requires strict equality (===) — matching value and type. assertEquals uses loose equality (==), allowing type coercion, so comparing an int to a numeric string can pass even though the types differ. assertSame should be the default for the same reason === is generally preferred over == in ordinary PHP code — it catches a real class of type-related bugs that a looser comparison would silently let slip through a passing test.

Q: What problem does mocking a dependency (with createMock) solve in a PHPUnit test, and why does it typically require depending on an interface? Mocking replaces a real dependency — one that's slow, has side effects, or is awkward to construct in a test — with a fake stand-in whose return values (and even call counts, via expects($this->once())) are controlled directly by the test, so the class under test can be verified in isolation without the real dependency ever actually running. This works cleanly when the class depends on an interface rather than a concrete class, since PHPUnit can generate a fake implementation of any interface on demand; mocking a concrete class directly is often fragile or impossible if it has a real constructor, side effects, or final methods in the way.