PHP OOP
The PHP object model is single implementation inheritance plus dispatch by name at runtime: when you call $obj->pay(), the engine looks the method up in the class table at the moment of the call — it does not consult a type table built ahead of time. That is where both the flexibility and every trap come from: an object that merely has the method fits anywhere, even if it implements no interface at all, and an interface type declaration is not what makes the call possible — it is what makes the contract checkable at the boundary instead of failing deep inside.
The dividing line of the topic is when a name gets resolved. Some mechanisms work at class-compile time: self:: binds permanently to the class the code is written in; use SomeTrait physically copies method bodies and properties into the using class (which is exactly why two traits with the same method are a fatal error, and exactly why insteadof and as exist); signature variance is checked by the engine when the class is linked. Others work at runtime: static:: (late static binding) resolves to the class the call was actually made on; __get/__set fire only for inaccessible properties, and at the price of being invisible to static analysis and IDE completion; instanceof walks the whole hierarchy. Keep the versions straight, too: property hooks and asymmetric visibility are PHP 8.4 (8.5 only extended asymmetric visibility to static properties), and readonly forbids reassignment even from inside the class — which is precisely the problem PHP 8.5's clone() function solves. The layer-by-layer breakdown is below.
Topic map
- Visibility and asymmetric visibility — why
privateis checked per class rather than per object, how it differs fromprotectedunder inheritance, and whatpublic private(set)from PHP 8.4 buys you. - Property declaration — typed properties and the uninitialized state, constructor property promotion, and property hooks that fire on every access.
- Interfaces and abstract classes — a stateless contract versus a partial base with state, why you may have many interfaces but only one parent, and how
instanceofwalks the hierarchy. - Traits and conflict resolution — the trait as a compile-time copy into the class, the "class > trait > parent" precedence order, and resolving a clash with
insteadofandas. - Static members and late static binding — a constant versus a
staticproperty, the slot shared with subclasses, and the topic's key distinction:self::versusstatic::. - Magic methods — what "inaccessible property" actually means, why
unset()opens the door to__get, and what magic costs you. - readonly and immutability — written exactly once from the declaring class scope, the shallowness of the freeze, and the
clone()function from PHP 8.5. - Enums — pure versus backed enums,
nameversusvalue,from()/tryFrom()at the boundary, and why cases are singletons. - Patterns and polymorphism — duck typing versus an explicit contract, a fluent chain typed
static, and a Singleton with thecloneandunserializeback doors closed.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
Thinking private blocks access from another instance of the same class | Visibility is checked per class, not per object — $other->amount inside a method works fine |
Confusing private(set) with readonly | Asymmetric visibility restricts who writes, not how many times — the class may keep writing |
| Dating property hooks and asymmetric visibility to PHP 8.5 | Both landed in 8.4; 8.5 only extended asymmetric visibility to static properties |
Treating a trait as a second extends with runtime inheritance | A trait is copied into the class at compile time; it is not a type, and instanceof never finds it |
| Expecting a trait method to override the class's own method | Precedence: the class's own method > the trait method > the inherited one — the class always wins |
| Pulling in two traits with the same method and expecting PHP to pick | A compile-time fatal error until the clash is resolved with insteadof / as |
Writing new self() in a parent factory | self:: is bound at compile time to the parent — Child::create() returns the parent, not Child |
Expecting a subclass to get its own copy of an inherited static property | It is one slot for the whole hierarchy: Child::$count++ also changes Base::$count |
Reading a class constant as $this->NAME | $this->NAME looks for a property; a constant is read as self::NAME / Foo::NAME |
Believing __get runs on every property read | Only for an inaccessible one — undeclared, unset(), or closed off by visibility |
Forgetting that isset() on a virtual property needs __isset | isset($obj->virtual) returns false even though __get would hand back a value |
Expecting readonly to freeze deeply | It freezes the reference: the object behind it is still mutable |
Believing in the clone $obj with { ... } syntax | That syntax does not exist in any PHP version — in 8.5 clone became a function |
Expecting a pure enum case to have a value | Only a backed enum (enum Status: string) has value; a pure case has only name |
Confusing from() with tryFrom() | from() throws a ValueError on an unknown scalar; tryFrom() returns null |
Typing a fluent method's return as self instead of static | The chain breaks in a subclass: the next call is dispatched against the parent type |
| Guarding a Singleton with a private constructor alone | Two back doors remain: clone $instance, and unserialize(), which does not call the constructor |
Interview relevance
PHP OOP is the section where a junior is separated from a middle by a single question: "how does self:: differ from static::?" The right answer is not "one is newer" but "one binds at compile time to the class the code is written in; the other resolves at runtime to the class the call was made on". The same move applies to the whole topic: the interviewer probes your execution model, not your vocabulary — what physically happens to a class when a trait is mixed into it, what exactly the engine counts as an "inaccessible" property, and at which precise moment a readonly field is allowed to receive a value.
Typical checks:
- The difference between
privateandprotectedunder inheritance, and that visibility is a property of the class, not the object. - What constructor property promotion does, and how property hooks differ from
__get. - When to reach for an interface and when for an abstract class, and why you may implement many interfaces.
- What a trait really is, and how a clash of two same-named methods is resolved.
self::versusstatic::, and whatnew static()returns from a parent factory.- The exact conditions under which
__get/__callfire, and what magic costs. - The guarantees of
readonly, its shallowness, and how 8.5 copies an object with one field changed. - Pure versus backed enums,
from()versustryFrom(). - A fluent chain, and guarding a Singleton against
cloneandunserialize.
Common wrong answer: "a trait is like an interface, only with an implementation." That belief leads straight to the next mistake: if it is an interface, then instanceof should find it, and the engine will surely sort out a name clash somehow. In reality a trait is a compile-time copy into the class: its members become the class's own members, instanceof knows nothing about it, and two identical methods are a fatal error. The second classic failure is promising immutability with readonly and then trying to build a withX() via clone plus assignment: after the clone the property is still initialized, and the assignment throws an Error. That exact scenario is what PHP 8.5's clone($this, ['statusCode' => $code]) exists for.