PHP Property Resolution Order: Class · Trait · Parent · Interface
PHP offers the same four places to define a property that it offers for constants: directly in a class, in a trait the class uses, in a parent class it extends, and, as of PHP 8.4, in an interface it implements. Since these different places may all happen at the same time, they are in competition. So, thre will be only one winner in the PHP property resolution order: Class, Trait, Parent, Interface
That last one used to be a flat “no”. For the entire history of the language before 8.4, interfaces could not declare properties at all; they were the one member kind interfaces had nothing to say about. Property hooks changed that, but not by making interfaces work like they do for constants. They introduced a third model, distinct from both.
There’s also a dimension constants never had to deal with: storage. A constant is a value: read it from anywhere in the hierarchy and you get one thing back, copied conceptually into whichever class resolves it. A property is a slot in an object, or a shared cell on a class, and where that slot lives is not always where you’d guess. Two of the surprises below come directly from that difference.
The priority stack
| Priority | Layer | Description |
|---|---|---|
| 1 highest | Class own property | Always wins. Defined directly in the class body, or as a promoted property. |
| 2 | Trait property | Used when the class has no own definition. Copied in at compile time: value-checked against the class’s own definition, but not against the parent’s. |
| 3 | Parent class property | Inherited when nothing more local claims the name. A “inherited” doesn’t always mean “copied”. See the static-property section below. |
| 4 new in 8.4 | Interface hooked property only | A plain property on an interface is a compile error. A hooked property is a contract, exactly like an interface method: the interface says the property must exist and be readable or writable or both; the implementing class supplies the actual logic. |
Tier 4 isn’t really competing with tiers 1–3 the way an interface constant competes with a parent constant. A hooked property in an interface has no storage and no default value of its own. It’s a shape, not a value. So there’s no version of the constants article’s “ambiguous, parent and interface both supply a value” fatal error here. Properties sidestep that entire category of conflict, at the cost of interfaces being locked out of the storage-based kind of property for good.
A concrete example
<?php
trait HasGreeting {
public string $greeting = 'Hello from trait';
}
class BaseGreeter {
public string $greeting = 'Hello from parent class';
}
class App extends BaseGreeter {
use HasGreeting;
public string $greeting = 'Hello from App itself';
}
echo (new App)->greeting;
// "Hello from App itself"
?>
This code actually fails, as the compatibility between the class and the trait definition MUST be complete: down to the default value. If the definition is not the same, it yields a “App and HasGreeting define the same property ($greeting) in the composition of %s. However, the definition differs and is considered incompatible. Class was composed” compilation error.
Now, remove App‘s own property and the trait takes over: "Hello from trait", with no complaint that it disagrees with the parent’s value: class-vs-parent and trait-vs-parent are silent overrides, not a checked ones. Remove the trait too, and App simply inherits BaseGreeter‘s property, no conflict, because nothing else at that level is competing for the name.
Interfaces couldn’t join this club, until PHP 8.4
<?php
interface Greeter {
public $greeting;
}
?>
Fatal error: Interfaces may only include hooked properties
?>
[/php]
That’s the whole story, for every PHP version before 8.4: an interface demanding a property was simply not expressible. Property hooks reopen the door, but as a contract for behavior, not storage:
<?php
interface Greeter {
public string $greeting { get; }
}
class App implements Greeter {
public string $greeting { get => 'Hello, hooked'; }
}
echo (new App)->greeting; // "Hello, hooked"
?>
This is structurally closer to how interfaces handle methods than how they handle constants: the interface declares that reading or writing $greeting must be possible, and leaves the actual value, computed, stored, whatever the implementer wants, entirely up to the class. Two interfaces can declare the same hooked property name without any risk of the “ambiguous” fatal error the constants article ran into, because neither interface is supplying a competing value to be ambiguous about.
The rules that catch people out
Trait beats parent, no questions asked
<?php
trait T { public $foo = 'trait-val'; }
class ParentC { public $foo = 'parent-val'; }
class ChildC extends ParentC { use T; }
echo (new ChildC)->foo; // "trait-val" — no error, despite disagreeing with the parent
?>
Same rule as constants: a trait property is compiled directly into the class body, so it’s treated as the class’s own definition the moment there’s nothing more local to contest it.
Class beats trait — but only if they agree, or the class overrides
<?php
trait T { public $foo = 'trait-val'; }
class C {
use T;
public $foo = 'class-val'; // different default
}
?>
Fatal error: C and T define the same property ($foo) in the
composition of C. However, the definition differs and is
considered incompatible.
?>
[/php]
Give the class the same default the trait has and it compiles fine. This value-sensitivity is specific to the class-vs-its-own-traits relationship: it doesn’t apply to trait-vs-parent, which never checks at all.
Two traits, same property: fatal only if the defaults differ
<?php
trait T1 { public $foo = 'one'; }
trait T2 { public $foo = 'two'; }
class C { use T1, T2; } // Fatal: differing definition, considered incompatible
?>
Identical defaults compile without complaint. As with constants, there’s no insteadof/as for properties: that syntax only resolves method conflicts. The only fix is declaring the property explicitly on the class.
Shadowing a private property doesn’t override it — it duplicates it
This is the one with no equivalent anywhere in the constants article, because constants aren’t storage and can’t be duplicated. Properties can:
<?php
class ParentF {
private $x = 'parent-x';
public function dumpParent() { return $this->x; }
}
class ChildF extends ParentF {
private $x = 'child-x';
public function dumpChild() { return $this->x; }
}
$obj = new ChildF();
var_dump($obj);
?>
object(ChildF)#1 (2) {
["x":"ParentF":private]=>
string(8) "parent-x"
["x":"ChildF":private]=>
string(7) "child-x"
}
?>
[/php]
Both $x properties are alive in the same object, in two separate storage slots, each visible only from methods declared in its own class. dumpParent() reads ParentF‘s slot, dumpChild() reads ChildF‘s slot — from the outside this looks like one property, but it’s genuinely two. This only happens with private; protected and public properties with the same name really do get overridden, one slot, like you’d expect.
Static properties: shared storage unless you redeclare
<?php
class ParentG { public static $count = 0; }
class ChildG extends ParentG {} // no redeclaration
ParentG::$count = 5;
echo ChildG::$count; // 5 — same cell
ChildG::$count = 99;
echo ParentG::$count; // 99 — writing through the child mutated the parent
?>
Skip redeclaring a static property in the child and it isn’t “inherited” in the copy sense at all: it’s the literal same storage cell, and writes through either class name are visible through the other. Redeclare it, even with the same value, and that link is severed:
<?php
class ParentG2 { public static $count = 0; }
class ChildG2 extends ParentG2 { public static $count = 0; } // separate storage now
?>
Nothing in the constants model prepares you for this, because constants have no concept of a write happening after declaration: this is a live, mutable, shareable cell, not a value baked in at compile time.
readonly blocks a subclass from re-touching an already-set property
<?php
class ParentI {
public readonly string $foo;
public function __construct() { $this->foo = 'parent-init'; }
}
class ChildI extends ParentI {
public function __construct() {
parent::__construct();
$this->foo = 'child-attempt'; // too late — already initialized
}
}
?>
Error: Cannot modify readonly property ParentI::$foo
?>
[/php]
The lock applies per-property, not per-class: once parent::__construct() has set it, no code anywhere, including a subclass constructor, gets a second write.
Uninitialized typed properties throw exceptions, they don’t default to null
<?php
class C { public int $x; }
echo (new C)->x;
?>
Error: Typed property C::$x must not be accessed before initialization
?>
[/php]
This isn’t strictly a resolution-order rule, but it’s a failure mode constants simply cannot have, so it is worth mentioning here. A constant always has a value the moment the class is compiled. A typed property without a default can exist, structurally, in a state where reading it is an error.
Calling up the chain: self:: vs static::
Same late-static-binding split as constants, but with real consequences now that static properties are shared storage rather than fixed values:
<?php
class ParentH {
public static $foo = 'parent';
public static function selfVal() { return self::$foo; }
public static function staticVal() { return static::$foo; }
}
class ChildH extends ParentH {
public static $foo = 'child';
}
echo ChildH::selfVal(); // "parent" — self:: is fixed to the declaring class
echo ChildH::staticVal(); // "child" — static:: resolves against the calling class
?>
self::$foo, written inside ParentH, always reads ParentH‘s cell, no matter which subclass calls the method. static::$foo reads whichever class’s cell is actually in play. Combined with the storage-sharing rule above, this means self:: and static:: can end up pointing at two different cells entirely, not just two different values.
Side note reminder: enums
This is the sharpest three-way contrast of the whole series. Enums can declare their own constants freely, and they can implement interfaces just like classes: but they cannot declare properties at all, of any kind:
<?php
enum Suit {
public static $counter = 0; // Fatal error: Enum Suit cannot include properties
case Hearts;
}
?>
Not instance properties, not static ones, not hooked ones: the baillon is total. An enum case is meant to be a fixed, singleton value; giving it mutable storage would undermine that guarantee, so PHP forbids the whole category rather than picking rules for it. Where the constants article found enums following the same rules as classes, properties are where enums diverge completely.
The mental model
For class constants, “closest wins” was nearly the whole story, with one wrinkle: same-distance conflicts, like two interfaces or two traits, need an explicit tiebreaker because there’s no storage to fall back on: only values, and values don’t merge. Properties keep the “closest wins” rule for which definition is visible, but add a second, independent question the constants model never had to ask: is it the same storage, or two different storages wearing the same name?
Public and protected inheritance answer that question the boring, expected way: one property, always redefined identically. Private inheritance answers it the surprising way: two properties, coexisting, as dizigot twins. Static properties answer it based on whether the child bothered to redeclare, one shared cell, or two independent ones. Interfaces, which fully participated in the constants story, sit almost entirely outside the properties story, contributing shape through hooks but never storage. Knowing which value resolves is only half of understanding a property; the other half is knowing which piece of memory it actually lives in.
In the end, it is a zoo with very diverse animals.

