PHP OOP
Object-oriented PHP — interfaces versus abstract classes, traits and conflict resolution, magic methods, visibility and late static binding, promoted and readonly properties, enums, static members, and the patterns interviewers ask you to write.
22 questions
JuniorTheoryVery commonWhat does instanceof check, and how does it treat interfaces and parents?
What does instanceof check, and how does it treat interfaces and parents?
$x instanceof Foo is true when $x is an object of class Foo, of a subclass, or of a class implementing the interface Foo — it walks the whole hierarchy, not just the exact class. On a non-object it returns false rather than throwing.
Common mistakes
- ✗Thinking
instanceofmatches only the exact class, not subclasses or interfaces - ✗Expecting an error instead of
falsewhen the left operand is not an object - ✗Using
instanceofagainst a scalar type such asstringorint
Follow-up questions
- →How does
instanceofdiffer fromget_class($x) === Foo::class? - →Why is a long chain of
instanceofchecks usually a design smell?
JuniorTheoryVery commonWhat is the difference between an abstract class and an interface in PHP?
What is the difference between an abstract class and an interface in PHP?
An interface is a pure contract — method signatures and constants, no state — and a class may implement many of them at once. An abstract class is a partial base with properties, a constructor and concrete methods, but a class extends only one.
Common mistakes
- ✗Thinking an interface can hold properties or a constructor and carry state
- ✗Believing PHP allows extending more than one abstract class
- ✗Declaring an interface when the goal is to share concrete implementation
Follow-up questions
- →When would an interface plus a trait with a default implementation beat an abstract class?
- →Can an abstract class implement an interface without defining all of its methods?
JuniorTheoryVery commonHow do private and protected differ once a class is extended?
How do private and protected differ once a class is extended?
protected members are visible in the declaring class and in every subclass. private members are visible only inside the declaring class, so a child cannot touch the parent's private property and may redeclare its own of the same name.
Common mistakes
- ✗Assuming a subclass can access the parent's
privatemembers - ✗Thinking
privateblocks access from another instance of the same class (it does not) - ✗Believing a redeclared
privatein a child collides with the parent's
Follow-up questions
- →Why can one instance read another instance's
privateproperty inside the same class? - →What does
privatevisibility mean for a method resolved through late static binding?
JuniorTheoryVery commonWhat is the difference between a static method and an instance method?
What is the difference between a static method and an instance method?
A static method belongs to the class, is called as Foo::bar() without an object, has no $this and can touch only static members. An instance method is called on an object, receives $this and reads that object's properties.
Common mistakes
- ✗Using
$thisinside astaticmethod - ✗Thinking
staticis about caching or performance rather than about scope - ✗Calling an instance method statically and expecting PHP to build an object
Follow-up questions
- →Why do static calls make a class harder to unit-test than injected collaborators?
- →What does
static::resolve to thatself::does not?
JuniorTheoryCommonWhen do you use a class constant instead of a static property in PHP?
When do you use a class constant instead of a static property in PHP?
A class constant is fixed, cannot be reassigned and is read as Foo::NAME. A static property is a single mutable slot shared by every instance, read as Foo::$name. Use a constant for a value that never changes; a static property is shared mutable state.
Common mistakes
- ✗Thinking a class constant can be reassigned at runtime
- ✗Expecting each subclass to get its own copy of an inherited
staticproperty - ✗Reading a class constant with
$this->NAMEinstead ofself::NAME
Follow-up questions
- →What happens to
Parent::$countwhen a subclass writes toChild::$countwithout redeclaring it? - →How does marking a class constant
finalchange what a subclass may do?
JuniorTheoryCommonWhat does constructor property promotion do to a PHP 8 constructor?
What does constructor property promotion do to a PHP 8 constructor?
Putting a visibility modifier on a constructor parameter — __construct(private Logger $log) — declares the property and assigns the argument in one step, removing the declare-then-$this->log = $log boilerplate. The result is an ordinary typed property.
Common mistakes
- ✗Thinking a promoted parameter still needs
$this->log = $login the constructor body - ✗Believing promotion works only with
publicvisibility - ✗Expecting a promoted property to be
staticor shared between instances
Follow-up questions
- →Can a promoted property also be
readonly, and what does that combination guarantee? - →Why can promotion not be used in a method other than
__construct?
JuniorTheoryCommonHow does a backed enum differ from a pure enum in PHP?
How does a backed enum differ from a pure enum in PHP?
A pure enum's cases are singletons carrying only a name. A backed enum declares a scalar type — enum Status: string — so each case also has a value and gains from()/tryFrom() to map a scalar back to a case. Back it when the value is persisted.
Common mistakes
- ✗Expecting a pure enum case to have a
value - ✗Swapping
from()(throws on an unknown scalar) withtryFrom()(returnsnull) - ✗Trying to instantiate an enum case with
new
Follow-up questions
- →How does an enum implementing an interface change what its cases can do?
- →Where should a human-readable label live, given a case carries only
nameandvalue?
JuniorTheoryCommonWhy can a PHP class implement many interfaces but extend only one class?
Why can a PHP class implement many interfaces but extend only one class?
PHP has single inheritance of implementation: a class extends one parent, so two parents can never supply conflicting method bodies. Interfaces carry no bodies and no state, so implements B, C merely merges contracts and cannot conflict.
Common mistakes
- ✗Thinking
extends A, Bis legal PHP - ✗Believing two interfaces declaring the same compatible signature clash
- ✗Reaching for a trait as a way to get a second parent class
Follow-up questions
- →What happens when two implemented interfaces declare the same method with different signatures?
- →How does a trait give code reuse without adding a second parent class?
JuniorTheoryCommonWhat does a readonly property guarantee once the object is constructed?
What does a readonly property guarantee once the object is constructed?
A readonly typed property can be written exactly once, from inside the declaring class — normally the constructor. Any later write throws an Error, even from within the class. It is shallow: the reference is frozen, but the object it points to stays mutable.
Common mistakes
- ✗Expecting
readonlyto deep-freeze the object a property points to - ✗Thinking the declaring class may still overwrite the property after construction
- ✗Trying to initialise a
readonlyproperty from outside the declaring class's scope
Follow-up questions
- →Why can a
readonlyproperty not be initialised from a subclass's constructor? - →How do you produce a modified copy of an object whose properties are all
readonly?
JuniorTheoryCommonWhat problem do traits solve in PHP given single class inheritance?
What problem do traits solve in PHP given single class inheritance?
A trait is a bag of methods and properties copied into a class at compile time, letting unrelated classes share an implementation without a common parent. It is horizontal reuse: a trait is not a type, is never instantiated, and instanceof never matches it.
Common mistakes
- ✗Thinking a trait is a type —
instanceofnever matches a trait - ✗Expecting a trait method to override the using class's own method (the class always wins)
- ✗Treating
useas a secondextendswith runtime inheritance
Follow-up questions
- →In what precedence order do the class's own, the trait's, and the parent's methods resolve?
- →What happens when two traits used by one class declare a property of the same name?
MiddleTheoryCommonHow does static:: differ from self:: inside an inherited method?
How does static:: differ from self:: inside an inherited method?
self:: binds at compile time to the class the code is written in, so an inherited method always hits the parent's version. static:: is late static binding — it resolves at runtime to the class actually called, so a parent's new static() reached via Child::create() builds a Child.
Common mistakes
- ✗Using
new self()in a parent factory and being surprised it never returns aChild - ✗Thinking
static::requires the enclosing method to be declaredstatic - ✗Expecting
self::to follow the class the call was actually made on
Follow-up questions
- →What does
get_called_class()return inside a parent method invoked asChild::run()? - →How does late static binding make an inheritable static factory possible?
MiddleTheoryCommonHow does the structural-typing idiom duck typing relate to polymorphism via an interface?
How does the structural-typing idiom duck typing relate to polymorphism via an interface?
PHP resolves a method call at runtime, so $x->pay() works on any object that happens to have pay() — that is duck typing, and polymorphism needs nothing more. Typing the parameter as an interface makes the contract explicit and checked at the boundary, turning a deep runtime Error into a TypeError at the door.
Common mistakes
- ✗Believing an object must implement an interface before its method can be called at all
- ✗Treating an interface type declaration as documentation that the engine does not enforce
- ✗Expecting a call to a missing method to return
nullinstead of throwing anError
Follow-up questions
- →What exactly does PHP throw when you call a method the object does not have?
- →When is an explicit interface worth adding over relying on the shape of the object?
JuniorCodeOccasionalBuild a fluent query builder that supports method chaining
Build a fluent query builder that supports method chaining
Every chainable method mutates the builder and returns $this, typed static so a subclass keeps chaining — that is what lets calls be strung together. toSql() is the terminal method: it returns the assembled string, not the builder, so the chain ends there.
Common mistakes
- ✗Forgetting to
return $this, so the next call in the chain lands onnull - ✗Typing the return as
selfinstead ofstatic, which breaks chaining in a subclass - ✗Making the terminal
toSql()return the builder instead of the assembled string
Follow-up questions
- →Why does
staticas a return type keep chaining working in a subclass whereselfwould not? - →How would you make the builder immutable so each call returns a modified clone?
JuniorTheoryOccasionalWhat are PHP magic methods, and when does the engine call __get or __call?
What are PHP magic methods, and when does the engine call __get or __call?
Magic methods are __-prefixed hooks the engine calls for you. __get/__set fire only when a property is undeclared or inaccessible from the calling scope; __call fires only when the method is missing. Others include __toString, __invoke and __clone.
Common mistakes
- ✗Thinking
__getruns on every property read, not just inaccessible or undeclared ones - ✗Expecting
__callto intercept methods that actually exist on the class - ✗Forgetting that
isset()on a virtual property needs__isset, not__get
Follow-up questions
- →Why does a declared but
privateproperty still trigger__getwhen read from outside the class? - →What does
__callStaticadd over__call?
MiddleTheoryOccasionalWhen does __get fire, and why prefer declared properties over it?
When does __get fire, and why prefer declared properties over it?
__get fires only when the property is undeclared, unset, or inaccessible from the calling scope — a declared, initialised public property never reaches it. Magic properties are invisible to static analysis and IDEs, cost a method call per access, and need __isset for isset() to work.
Common mistakes
- ✗Thinking
__getruns on every property read, including declared public ones - ✗Forgetting that
isset()on a magic property needs__isset - ✗Not realising that
unset()ing a declared property makes later reads go through__get
Follow-up questions
- →Why does
unset($obj->declared)make later reads of that property go through__get? - →How do PHP 8.4 property hooks cover the same use case without
__get?
MiddleCodeOccasionalGuard the creational pattern Singleton against duplication by clone and unserialize
Guard the creational pattern Singleton against duplication by clone and unserialize
Make __construct private so new is blocked, keep the instance in a private static slot, and hand it out from getInstance(). Then close two back doors: a private __clone() makes clone fatal, and __wakeup() must throw, since unserialize() rebuilds an object without the constructor.
Common mistakes
- ✗Blocking
newwith a private constructor but leavingclone $instancewide open - ✗Forgetting that
unserialize()rebuilds an object without ever calling__construct - ✗Believing
finalon the class is what prevents cloning
Follow-up questions
- →Why does
unserialize()bypass a private constructor, and what does__wakeup()do about it? - →What makes a singleton hard to test, and what would you inject instead?
MiddleTheoryOccasionalHow do you resolve two traits that declare a method of the same name?
How do you resolve two traits that declare a method of the same name?
Two traits declaring the same method is a fatal error unless the use block disambiguates: insteadof picks which trait's method wins, and as gives the loser an alias so it stays reachable. as can also change the visibility of the imported method.
Common mistakes
- ✗Assuming PHP silently picks one of the two conflicting trait methods
- ✗Thinking
asselects the winner andinsteadofrenames (they are the other way round) - ✗Forgetting that
ascan also change the visibility of an imported method
Follow-up questions
- →What happens when the class itself declares a method that a used trait also declares?
- →How does a conflicting trait property differ from a conflicting trait method?
MiddleTheoryRareWhat does asymmetric visibility such as public private(set) give a property?
What does asymmetric visibility such as public private(set) give a property?
It splits read visibility from write visibility: public private(set) int $total is readable anywhere but writable only inside the class. Unlike readonly, the class may keep changing it — the limit is on who writes, not how often. PHP 8.4 added it; 8.5 extended it to static properties.
Common mistakes
- ✗Confusing
private(set)withreadonly— the declaring class may still write to it repeatedly - ✗Reading
public private(set)as restricting reads rather than writes - ✗Assuming asymmetric visibility arrived in PHP 8.5 rather than in 8.4
Follow-up questions
- →When would you choose
private(set)over making the propertyreadonly? - →What does
protected(set)allow a subclass to do thatprivate(set)forbids?
MiddleTheoryRareHow does PHP 8.5 let you copy a readonly object with one field changed?
How does PHP 8.5 let you copy a readonly object with one field changed?
PHP 8.5 turned clone into a function too: clone(object $object, array $withProperties = []): object. Called inside the class as clone($this, ['statusCode' => $code]), it copies the object and applies those properties during the clone — the one moment a readonly field may be re-set.
Common mistakes
- ✗Believing PHP has a
clone $obj with { ... }syntax —clonebecame a function, not a keyword with awithclause - ✗Assigning to a
readonlyproperty on the freshly cloned object from outside the declaring class - ✗Expecting
readonlyto be reassignable from inside the class at any time, rather than only while cloning
Follow-up questions
- →Why must
clone($this, [...])be called from inside the class rather than by the caller holding the object? - →What did a modified copy of a
readonlyvalue object cost before this existed?
MiddleTheoryRareWhat do property hooks, added in PHP 8.4, change about a declared property?
What do property hooks, added in PHP 8.4, change about a declared property?
A hook attaches get/set logic to a declared property, so reading $obj->total runs code while the member stays an ordinary typed property. Unlike __get, the property really exists and stays visible to static analysis, and the hook fires on every access, not only an inaccessible one.
Common mistakes
- ✗Thinking a hooked property is virtual and undeclared, the way a
__getproperty is - ✗Expecting a hook to fire only when the property is inaccessible or unset
- ✗Placing property hooks in PHP 8.5 rather than in 8.4, where they landed
Follow-up questions
- →How does a hooked property keep working with static analysers where a
__getproperty does not? - →Can a hooked property still be promoted in the constructor, and what does the
sethook receive?
SeniorDesignRareYou inherit a 60k-line PHP application written procedurally — a directory of include files, global functions, superglobals read directly, and SQL strings built inline. There are no tests, it is live in production, and the business will not fund a rewrite. You are asked to make it testable and to move it toward an object-oriented, dependency-injected architecture, incrementally, with no feature freeze. Describe how you would approach it: where you would start, how you would carve seams into procedural code, how you would introduce classes and interfaces without breaking existing call sites, how you would get the first tests running against code that is currently untestable, and what stops the migration from stalling half-done.
You inherit a 60k-line PHP application written procedurally — a directory of include files, global functions, superglobals read directly, and SQL strings built inline. There are no tests, it is live in production, and the business will not fund a rewrite. You are asked to make it testable and to move it toward an object-oriented, dependency-injected architecture, incrementally, with no feature freeze. Describe how you would approach it: where you would start, how you would carve seams into procedural code, how you would introduce classes and interfaces without breaking existing call sites, how you would get the first tests running against code that is currently untestable, and what stops the migration from stalling half-done.
Pin behaviour first: characterization tests driven through the HTTP entry point, so any break is visible. Then carve seams — wrap each global function or superglobal read behind a thin class with an interface, and leave the old function as a one-line adapter so existing call sites keep working. Convert leaf-first: extract a service, inject its collaborators, delete the global.
Common mistakes
- ✗Proposing a big-bang rewrite behind a freeze that the business will never fund
- ✗Turning global functions into
staticmethods and calling the result dependency injection - ✗Writing tests only after the refactor, so nothing pins the original behaviour while it changes
Follow-up questions
- →How do you test a function that reads
$_SESSIONdirectly, without touching its call sites? - →What CI rule would stop the procedural surface from growing while the migration runs?