Re: [PHP-DEV] Protected inheritance hierarchies

On Tue, Jul 29, 2025, at 20:11, Jonathan Vollebregt wrote:

I came across this edge case today:

https://3v4l.org/R3Q8D

Both psalm and phpstan think this code is A-OK (Once you add the
requisite type hints) but it causes fatal errors way back to PHP 5.0.0

I believe classes should be able to access protected properties on their
siblings if the property in question was declared by a shared parent,
but it seems both the engine and reflection (ie. getDeclaringClass)
think that redeclaring a protected property makes it a property of the
child, not the parent.

This is particularly confusing since the parent class can access the
child class’ redeclared protected property, only the sibling can’t.

Properties were invariant until the introduction of property hooks, so
the only edge cases I can think of would be in property hooks (But when
the input is correctly typed this shouldn’t be a problem either)

Is there a technical reason for this behavior or would it be possible to
relax this?

  • Jonathan

It’s not an edge case, in C2, you redefine a protected variable with the same name and shadowed the original $v. That $v is different than C’s $v. It’s easiest to see this with static access: https://3v4l.org/0SRWb#v8.4.10

However, I don’t know of any way to unshadow a property from $this to access the ancestor’s value (other than using private access), but it exists and takes up memory; just accessing it is the hard part.

— Rob

On Sat, Aug 2, 2025 at 12:10 PM Rob Landers rob@bottled.codes wrote:

On Tue, Jul 29, 2025, at 20:11, Jonathan Vollebregt wrote:

I came across this edge case today:

https://3v4l.org/R3Q8D

Both psalm and phpstan think this code is A-OK (Once you add the
requisite type hints) but it causes fatal errors way back to PHP 5.0.0

……

It’s not an edge case, in C2, you redefine a protected variable with the same name and shadowed the original $v. That $v is different than C’s $v. It’s easiest to see this with static access: https://3v4l.org/0SRWb#v8.4.10

However, I don’t know of any way to unshadow a property from $this to access the ancestor’s value (other than using private access), but it exists and takes up memory; just accessing it is the hard part.

— Rob

Hi Rob,

I’m pretty sure that there is no shadowing happening in the example.
When the child instance is created, there is just one slot for the property, as the child one replaces the parent one.
So basically the child property overrides the parent property rather than shadowing it.

True shadowing (two slots) only occurs when the parent property is declared private.

It’s just that when redefining, it stores the declaring class, and so there is this sibling class access issue.

I’m wondering now if the access shouldn’t be relaxed, in case we have the parent class that initially defined the property.

Of course, we should focus on non-static properties, as static ones are different things, and there is some shadowing there.


Alex

On Sat, Aug 2, 2025, at 16:04, Alexandru Pătrănescu wrote:

On Sat, Aug 2, 2025 at 12:10 PM Rob Landers rob@bottled.codes wrote:

On Tue, Jul 29, 2025, at 20:11, Jonathan Vollebregt wrote:

I came across this edge case today:

https://3v4l.org/R3Q8D

Both psalm and phpstan think this code is A-OK (Once you add the
requisite type hints) but it causes fatal errors way back to PHP 5.0.0

……

It’s not an edge case, in C2, you redefine a protected variable with the same name and shadowed the original $v. That $v is different than C’s $v. It’s easiest to see this with static access: https://3v4l.org/0SRWb#v8.4.10

However, I don’t know of any way to unshadow a property from $this to access the ancestor’s value (other than using private access), but it exists and takes up memory; just accessing it is the hard part.

— Rob

Hi Rob,

I’m pretty sure that there is no shadowing happening in the example.

When the child instance is created, there is just one slot for the property, as the child one replaces the parent one.
So basically the child property overrides the parent property rather than shadowing it.

True shadowing (two slots) only occurs when the parent property is declared private.

It’s just that when redefining, it stores the declaring class, and so there is this sibling class access issue.

I’m wondering now if the access shouldn’t be relaxed, in case we have the parent class that initially defined the property.

Of course, we should focus on non-static properties, as static ones are different things, and there is some shadowing there.


Alex

Hi Alex,

I’m not sure what you mean? https://3v4l.org/WKILh#v8.4.10

There is clearly shadowing going on.

— Rob

On Sat, Aug 2, 2025, 17:10 Rob Landers rob@bottled.codes wrote:

On Sat, Aug 2, 2025, at 16:04, Alexandru Pătrănescu wrote:

On Sat, Aug 2, 2025 at 12:10 PM Rob Landers rob@bottled.codes wrote:

On Tue, Jul 29, 2025, at 20:11, Jonathan Vollebregt wrote:

I came across this edge case today:

https://3v4l.org/R3Q8D

Both psalm and phpstan think this code is A-OK (Once you add the
requisite type hints) but it causes fatal errors way back to PHP 5.0.0

……

It’s not an edge case, in C2, you redefine a protected variable with the same name and shadowed the original $v. That $v is different than C’s $v. It’s easiest to see this with static access: https://3v4l.org/0SRWb#v8.4.10

However, I don’t know of any way to unshadow a property from $this to access the ancestor’s value (other than using private access), but it exists and takes up memory; just accessing it is the hard part.

— Rob

Hi Rob,

I’m pretty sure that there is no shadowing happening in the example.

When the child instance is created, there is just one slot for the property, as the child one replaces the parent one.
So basically the child property overrides the parent property rather than shadowing it.

True shadowing (two slots) only occurs when the parent property is declared private.

It’s just that when redefining, it stores the declaring class, and so there is this sibling class access issue.

I’m wondering now if the access shouldn’t be relaxed, in case we have the parent class that initially defined the property.

Of course, we should focus on non-static properties, as static ones are different things, and there is some shadowing there.


Alex

Hi Alex,

I’m not sure what you mean? https://3v4l.org/WKILh#v8.4.10

There is clearly shadowing going on.

Hi Rob,

As I said, let’s leave aside the static case, as the question from Jonathan was not about that.

Given the class P that defines a protected property with value 1,
and a class C that extends P and re-defines the protected property with the value 2,
please show me an example where you could get from an instance of class C the value 1 of the parent class property that you think it’s shadowed.
Bonus point, if you manage that, you could also set it to something else, and so have a hidden storage for any object of class C that is not really visible normally.

As far as I know, there is no way to achieve that, and the reason is because at runtime the objects have a single slot for the protected property; the child class property overrides the parent class property when redeclared, and does not shadow it.
But please prove me wrong.

Thanks,
Alex

On 02/08/2025 15:09, Rob Landers wrote:

I’m not sure what you mean? Online PHP editor | output for WKILh

There is clearly shadowing going on.

There's a lot of confusing code in that example, so I'm not really sure what it's illustrating.

This example seems more to the point: Online PHP editor | output for FVhXa

Regardless of whether you look up the reflection property on the parent or the child, it points to the same slot for property 'v'

In contrast, the same reflection code for a private property finds both values stored in the object: Online PHP editor | output for S7GmM

You can see the same result with var_dump (and serialize, and debug_zval_dump, ...): Online PHP editor | output for FqecC

I think the relevant code is this, in zend_declare_typed_property: zend_API.c (revision 78d96e94fa8e05dd59d03aa4891fa843ebc93ef8) - OpenGrok cross reference for /php-src/Zend/zend_API.c

if (access_type & ZEND_ACC_PUBLIC) {
property_info->name = zend_string_copy(name);
} else if (access_type & ZEND_ACC_PRIVATE) {
property_info->name = zend_mangle_property_name(ZSTR_VAL(ce->name), ZSTR_LEN(ce->name), ZSTR_VAL(name), ZSTR_LEN(name), is_persistent_class(ce));
} else {
ZEND_ASSERT(access_type & ZEND_ACC_PROTECTED);
property_info->name = zend_mangle_property_name("*", 1, ZSTR_VAL(name), ZSTR_LEN(name), is_persistent_class(ce));
}

For a private name, the mangled name includes the class where it's defined, which is where the shadowing comes from: the final merged symbol table has two differently named slots.

But for a protected name, the prefix is just "*", so it's the same slot no matter how many times it appears in the hierarchy, just like for public properties (whose names aren't mangled at all).

How this relates back to the original example in the thread, I'm not sure, but it's definitely not "shadowing" in the same sense as a private property.

--
Rowan Tommins
[IMSoP]

On Sat, Aug 2, 2025, at 19:04, Alexandru Pătrănescu wrote:

On Sat, Aug 2, 2025, 17:10 Rob Landers rob@bottled.codes wrote:

On Sat, Aug 2, 2025, at 16:04, Alexandru Pătrănescu wrote:

On Sat, Aug 2, 2025 at 12:10 PM Rob Landers rob@bottled.codes wrote:

On Tue, Jul 29, 2025, at 20:11, Jonathan Vollebregt wrote:

I came across this edge case today:

https://3v4l.org/R3Q8D

Both psalm and phpstan think this code is A-OK (Once you add the
requisite type hints) but it causes fatal errors way back to PHP 5.0.0

……

It’s not an edge case, in C2, you redefine a protected variable with the same name and shadowed the original $v. That $v is different than C’s $v. It’s easiest to see this with static access: https://3v4l.org/0SRWb#v8.4.10

However, I don’t know of any way to unshadow a property from $this to access the ancestor’s value (other than using private access), but it exists and takes up memory; just accessing it is the hard part.

— Rob

Hi Rob,

I’m pretty sure that there is no shadowing happening in the example.

When the child instance is created, there is just one slot for the property, as the child one replaces the parent one.
So basically the child property overrides the parent property rather than shadowing it.

True shadowing (two slots) only occurs when the parent property is declared private.

It’s just that when redefining, it stores the declaring class, and so there is this sibling class access issue.

I’m wondering now if the access shouldn’t be relaxed, in case we have the parent class that initially defined the property.

Of course, we should focus on non-static properties, as static ones are different things, and there is some shadowing there.


Alex

Hi Alex,

I’m not sure what you mean? https://3v4l.org/WKILh#v8.4.10

There is clearly shadowing going on.

Hi Rob,

As I said, let’s leave aside the static case, as the question from Jonathan was not about that.

Given the class P that defines a protected property with value 1,
and a class C that extends P and re-defines the protected property with the value 2,
please show me an example where you could get from an instance of class C the value 1 of the parent class property that you think it’s shadowed.
Bonus point, if you manage that, you could also set it to something else, and so have a hidden storage for any object of class C that is not really visible normally.

As far as I know, there is no way to achieve that, and the reason is because at runtime the objects have a single slot for the protected property; the child class property overrides the parent class property when redeclared, and does not shadow it.
But please prove me wrong.

Thanks,
Alex

I mentioned in my first reply, there is no way to get an instance-level property unshadowed. It is there though (according to inheritance.c, if I’m reading it right, it is still accessible, just not from user-land).

In any case, there are lots of interesting footguns with properties and inheritance: Problem with abstract nested object · Issue #47 · Crell/Serde.

Could it be considered a bug that my first example produces a fatal
error instead of trying to access the shadowed parent property that it
has access to and is typed to use?

Other languages (such as C#, Java, etc: https://www.programiz.com/online-compiler/0ud6UO24mHOTU) don’t allow you to access protected properties/methods on sibling classes. This is because “protected” is usually used in the context of inheritance; access is usually restricted to “myself” or “children” and a sibling is neither of those. If there is a bug, the bug is that you can access a sibling’s protected properties, at all.

— Rob

In 2006 the absence of this feature was fixed as a bug and meged in PHP 5.2: https://bugs.php.net/bug.php?id=37632
In 2020 Nikita Popov agreed that this is expected: https://x.com/nikita_ppv/status/1261633126687805440

So one way is to explicitly mention this feature in the Visibility docs and fix the redeclaration issue to make things consistent.

The other way is to deprecate sibling relations.

···

Valentin

On 2 August 2025 20:12:59 BST, Rob Landers <rob@bottled.codes> wrote:

I mentioned in my first reply, there is no way to get an instance-level property unshadowed. It is there though (according to inheritance.c, if I’m reading it right, it is still accessible, just not from user-land).

I'd be interested to see what code you're looking at. As I showed in my last email, there's an explicit difference in how private and protected property names are mangled, and it's consistent with how reflection, serialisation, and debug functions output them - which is that there is only one property, no matter how many times in the inheritance chain it is redefined.

Rowan Tommins
[IMSoP]

On Sat, Aug 2, 2025, at 22:33, Rowan Tommins [IMSoP] wrote:

On 2 August 2025 20:12:59 BST, Rob Landers <rob@bottled.codes> wrote:

I mentioned in my first reply, there is no way to get an instance-level property unshadowed. It is there though (according to inheritance.c, if I’m reading it right, it is still accessible, just not from user-land).

I’d be interested to see what code you’re looking at. As I showed in my last email, there’s an explicit difference in how private and protected property names are mangled, and it’s consistent with how reflection, serialisation, and debug functions output them - which is that there is only one property, no matter how many times in the inheritance chain it is redefined.

Rowan Tommins
[IMSoP]

If this were the case, then creating a base class with default values wouldn’t be possible. The memory exists and is set aside for that. The child class shadows this value, but the original value still exists. You can get to it by calling into the parent class entry and accessing it that way. There is no way to get to that value from an instance in php, though — from the perspective of the child, shadowing loses the original default value.

— Rob

On Sat, Aug 2, 2025, at 22:18, Valentin Udaltsov wrote:

On Sat, Aug 2, 2025, at 22:17, Rob Landers rob@bottled.codes wrote:

On Sat, Aug 2, 2025, at 19:04, Alexandru Pătrănescu wrote:

On Sat, Aug 2, 2025, 17:10 Rob Landers rob@bottled.codes wrote:

On Sat, Aug 2, 2025, at 16:04, Alexandru Pătrănescu wrote:

On Sat, Aug 2, 2025 at 12:10 PM Rob Landers rob@bottled.codes wrote:

On Tue, Jul 29, 2025, at 20:11, Jonathan Vollebregt wrote:

I came across this edge case today:

https://3v4l.org/R3Q8D

Both psalm and phpstan think this code is A-OK (Once you add the
requisite type hints) but it causes fatal errors way back to PHP 5.0.0

……

It’s not an edge case, in C2, you redefine a protected variable with the same name and shadowed the original $v. That $v is different than C’s $v. It’s easiest to see this with static access: https://3v4l.org/0SRWb#v8.4.10

However, I don’t know of any way to unshadow a property from $this to access the ancestor’s value (other than using private access), but it exists and takes up memory; just accessing it is the hard part.

— Rob

Hi Rob,

I’m pretty sure that there is no shadowing happening in the example.

When the child instance is created, there is just one slot for the property, as the child one replaces the parent one.
So basically the child property overrides the parent property rather than shadowing it.

True shadowing (two slots) only occurs when the parent property is declared private.

It’s just that when redefining, it stores the declaring class, and so there is this sibling class access issue.

I’m wondering now if the access shouldn’t be relaxed, in case we have the parent class that initially defined the property.

Of course, we should focus on non-static properties, as static ones are different things, and there is some shadowing there.


Alex

Hi Alex,

I’m not sure what you mean? https://3v4l.org/WKILh#v8.4.10

There is clearly shadowing going on.

Hi Rob,

As I said, let’s leave aside the static case, as the question from Jonathan was not about that.

Given the class P that defines a protected property with value 1,
and a class C that extends P and re-defines the protected property with the value 2,
please show me an example where you could get from an instance of class C the value 1 of the parent class property that you think it’s shadowed.
Bonus point, if you manage that, you could also set it to something else, and so have a hidden storage for any object of class C that is not really visible normally.

As far as I know, there is no way to achieve that, and the reason is because at runtime the objects have a single slot for the protected property; the child class property overrides the parent class property when redeclared, and does not shadow it.
But please prove me wrong.

Thanks,
Alex

I mentioned in my first reply, there is no way to get an instance-level property unshadowed. It is there though (according to inheritance.c, if I’m reading it right, it is still accessible, just not from user-land).

In any case, there are lots of interesting footguns with properties and inheritance: Problem with abstract nested object · Issue #47 · Crell/Serde.

Could it be considered a bug that my first example produces a fatal
error instead of trying to access the shadowed parent property that it
has access to and is typed to use?

Other languages (such as C#, Java, etc: https://www.programiz.com/online-compiler/0ud6UO24mHOTU) don’t allow you to access protected properties/methods on sibling classes. This is because “protected” is usually used in the context of inheritance; access is usually restricted to “myself” or “children” and a sibling is neither of those. If there is a bug, the bug is that you can access a sibling’s protected properties, at all.

— Rob

If there is a bug, the bug is that you can access a sibling’s protected properties, at all.

In 2006 the absence of this feature was fixed as a bug and meged in PHP 5.2: https://bugs.php.net/bug.php?id=37632

In 2020 Nikita Popov agreed that this is expected: https://x.com/nikita_ppv/status/1261633126687805440

So one way is to explicitly mention this feature in the Visibility docs and fix the redeclaration issue to make things consistent.

The other way is to deprecate sibling relations.

Valentin

I don’t think the redeclaration is a bug though, as is mentioned in the linked bug report about properties: https://bugs.php.net/bug.php?id=37212, it is pretty clear to me that people expect a redeclaration would be a fatal error, but not accessing a property declared in a shared parent scope (emphasis mine):

The property is not being redeclared in C, though. It is still a property of A, structure-wise. A method declared and called in the same way as the property does not cause any error.

This has been the case for years, so I don’t think it is a bug. I was only saying that if there is a bug, the bug would be that you can access another class’s protected properties that aren’t a parent or sibling. I didn’t really go into why, but IMHO, it breaks LSP, since it allows sibling classes to depend on each other’s internals, breaking substitutability (particularly in regards to hooks).

— Rob

On 2 August 2025 21:59:20 BST, Rob Landers <rob@bottled.codes> wrote:

If this were the case, then creating a base class with default values wouldn’t be possible. The memory exists and is set aside for that.

Sure it would: the default value is just an assignment that happens at a particular point of the object's lifecycle. For a child class which overrides the default of a parent (on a public or protected property), only the child class's assignment will ever be visible. So it would be perfectly valid for the class entry for the child class to only store that one assignment. I don't know if that actually happens; maybe the cost of de-duplicating is not seen as worthwhile, and the assignments are just run in sequence every time.

Regardless, the philosophical question in this thread seems to be whether re-declaring a protected property should change the "ownership" of that property. I think it's natural that a protected property *only* declared in a sibling class can't be accessed, so some ownership needs to be tracked.

What seems surprising is that the ownership changes if the same property is re-declared, especially since the new declaration has to match the original (e.g. you can't change the type), and every possible access tells the user the two declarations have been completely merged.

Intuitively, an identical declaration with no change other than a default value looks like it would be the same as overwriting the default in a constructor, but that is not the case. (Online PHP editor | output for 5iIak vs Online PHP editor | output for rL8pX)

I'm inclined to agree that this is a bug, regardless of whether it's difficult to fix in the implementation.

Rowan Tommins
[IMSoP]

On Sun, Aug 3, 2025, at 11:10, Rowan Tommins [IMSoP] wrote:

On 2 August 2025 21:59:20 BST, Rob Landers <rob@bottled.codes> wrote:

If this were the case, then creating a base class with default values wouldn’t be possible. The memory exists and is set aside for that.

Sure it would: the default value is just an assignment that happens at a particular point of the object’s lifecycle. For a child class which overrides the default of a parent (on a public or protected property), only the child class’s assignment will ever be visible. So it would be perfectly valid for the class entry for the child class to only store that one assignment. I don’t know if that actually happens; maybe the cost of de-duplicating is not seen as worthwhile, and the assignments are just run in sequence every time.

Regardless, the philosophical question in this thread seems to be whether re-declaring a protected property should change the “ownership” of that property. I think it’s natural that a protected property only declared in a sibling class can’t be accessed, so some ownership needs to be tracked.

What seems surprising is that the ownership changes if the same property is re-declared, especially since the new declaration has to match the original (e.g. you can’t change the type), and every possible access tells the user the two declarations have been completely merged.

Intuitively, an identical declaration with no change other than a default value looks like it would be the same as overwriting the default in a constructor, but that is not the case. (https://3v4l.org/5iIak vs https://3v4l.org/rL8pX)

I’m inclined to agree that this is a bug, regardless of whether it’s difficult to fix in the implementation.

Rowan Tommins
[IMSoP]

I’m not sure that this is a bug. You can redeclare the same type and add hooks (or change them), which breaks all assumptions about substitutability.

class A {
protected int $v = 2;
}

class B extends A {
public function getValue(A $v): void {
echo $v->v;
}
}

class C extends A {
protected int $v { set => $this->v * 2; }
}

$b = new B;
$c = new C;
$b->getValue($b);
$b->getValue($c);

C changes all assumptions from B’s point of view (technically C is a violation of LSP from the perspective of A, thus it should not pretend to be substitutable as A from the perspective of B when used in this way – though other properties/methods may in fact be substitutable and be useful).

— Rob

On 3 August 2025 10:30:13 BST, Rob Landers <rob@bottled.codes> wrote:

I'm not sure that this is a bug. You can redeclare the same type and add hooks (or change them), which breaks all assumptions about substitutability.

If substitutability was the problem, access to the re-declared property should be forbidden to the parent class as well, but it's not: Online PHP editor | output for OEadK

In fact, there's no break in the *contract* of the property there, only the *implementation*, just like a method can be redefined to have completely different behaviour.

If anything, refusing access to a sibling class is a break in contact: in the original example, class C is written with the expectation that property $v will be visible to it, for any instance of P it receives.

class P {
    protected $v = 1;
}
class C extends P {
    public function test(P $i) {
        return $i->v;
    }
}

This expectation holds, and appears to be a valid contract, regardless of whether the object given is an instance of P itself or of a subclass.

But, a completely unrelated class can create an object which satisfies instanceof P, but forbids access to property $v:

class C2 extends P {
    protected $v = 2;
}

Why should C2 have the right to break the expectation of C in that way?

Rowan Tommins
[IMSoP]

I originally wrote a long af reply … but now I agree that this is a bug as well after writing it out …

I’ll be referring to the following code:

class A {
protected int $v = 1;
}

class B extends A {}

class C extends A {
protected int $v = 2;
}

class D extends A {
protected int $v = 3 { get => $this->v * 2 }
}


Reason 1:
Changing the value changes the contract of it’s parent.

This is also possible indirectly without changing the contract in C:

class C extends A {
public function __construct() {
$this->v = 2;
}
}

This is effectively the same thing as redeclaring it and setting a different value for $v.


Reason 2:
Changing the behaviour changes the contract of it’s parent.

This is also possible indirectly without changing the contract in D:

class D extends A {
private int $v;
public function __construct() {
$this->v = $this->v;
unset($this->v);
}
public function __set($name, $value) {
$name = "
{$name}";
$this->{$name} = $value;
}
public function __get($name) {
$name = "
{$name}";
return $this->{$name};
}
}

Thus I think since this is already possible, making it more explicit vs. this wordy and round-about way of doing the same thing shouldn’t be necessary.


In both the examples above, a sibling class would still be able to access $v, even though the default value and/or behaviour change completely. However, if you make it more obvious that this is the case (by using hooks and/or changing the default value), you can’t access it from a sibling.

On Sunday 03 August 2025 11:30:13 (+02:00), Rob Landers wrote:

> I'm not sure that this is a bug. You can redeclare the same type and add hooks (or change them), which breaks all assumptions about substitutability.

Rob, maybe you can lecture me a bit: Isn't substitutability on the public interface / protocol only? What am I not seeing in your example?

Btw, I found the fatal error as well (and then useful, because moving a protected property upwards, PHP tells where duplicates are downwards in the hierarchy IIRC), however I'm also looking for something more internal for user land class and objects trees and the subject caught my attention.

-- hakre

On Wed, Aug 6, 2025, at 13:26, Hans Krentel wrote:

On Sunday 03 August 2025 11:30:13 (+02:00), Rob Landers wrote:

I’m not sure that this is a bug. You can redeclare the same type and add
hooks (or change them), which breaks all assumptions about
substitutability.

Rob, maybe you can lecture me a bit: Isn’t substitutability on the public
interface / protocol only? What am I not seeing in your example?

From a child class’s perspective, the “public interface” is both protected + public of the parent. If you change or misuse a parent’s implementation or invariants, it violates LSP, even if it doesn’t affect external clients immediately.

Take for example:

class Fruit {
public string $kind { get => “fruit” }
}

class Vegetable extends Fruit {
public string $kind { get => “vegetable” }
}

function foo(Fruit $fruit) {}

foo(new Vegetable); // hmmm

This is a “soft” violation that only makes sense to us humans, but PHP allows it. It requires us humans to realize we are performing an LSP violation and refactor the code so that we don’t pass a Carrot to someone expecting a Mango.

This can be done through protected means as well (simply replace the properties above as protected properties used internally), and it won’t be as obvious to consumers outside the class, but still there, nonetheless.

Btw, I found the fatal error as well (and then useful, because moving a
protected property upwards, PHP tells where duplicates are downwards in the
hierarchy IIRC), however I’m also looking for something more internal for
user land class and objects trees and the subject caught my attention.

– hakre

— Rob

On Wed, Aug 6, 2025, at 23:20, Jonathan Vollebregt wrote:

On 8/6/25 1:41 PM, Rob Landers wrote:

Take for example:

class Fruit {
public string $kind { get => “fruit” }
}

class Vegetable extends Fruit {
public string $kind { get => “vegetable” }
}

function foo(Fruit $fruit) {}

foo(new Vegetable); // hmmm

This is a “soft” violation that only makes sense to us humans, but PHP
allows it. It requires us humans to realize we are performing an LSP
violation and refactor the code so that we don’t pass a Carrot to
someone expecting a Mango.

Rob, that’s not how types work. The only thing guaranteed by this
definition of $fruit->kind is that it has a getter that returns a
string. “vegetable” is a string. This isn’t a violation at all.

“If S is a subtype of T, then objects of type T may be replaced with objects of type S (without altering the desirable properties of the program — correctness, task performed, etc.).”

Thanks Jonathan,

I agree that, from a type-system perspective, "vegetable" is a valid string and thus satisfies the declared contract of Fruit::$kind. But the point of LSP isn’t just about type compatibility — it is about the behavioural expectations established by the base class.

If consumers of Fruit rely (even implicitly) on $kind always being "fruit" (the property is essentially an instance-level constant here), then changing that in a subclass makes the code harder to reason about and maintain.

The language can’t catch these human-level expectations: that is up to code reviews and architectural design. My example is intentionally “soft” to show how semantic surprises can creep in even when the type checker is happy. That is ultimately why I feel like this issue is a bug. A language can’t check for every possible LSP violation. It probably shouldn’t as there are diminishing returns beyond a certain point and there are valid reasons to ignore LSP.

Now if you were able to type specific values in PHP a la psalm/phpstan
and make something like this:

class Fruit {
public “apple”|“pear” $kind { get => “fruit” }
}

Well then yeah Vegetable would be a violation, but this would be a
different (and more specific) type than just string. (And this is why
you can’t extend enums, it would widen a contract)

This isn’t a “soft” violation that “PHP allows”, this is a well defined
property of every programming language I know of.

Right, so if the language was expressive enough (like with literal types or enums), then changing $kind would clearly violate the contract. In other words, you’re agreeing with me that this would be a violation in a stricter language or type system, so the only difference here is the tools we have available, not the underlying logic.

Ultimately, my goal was to illustrate that the principle of LSP is always present, regardless of whether the language enforces it for you. The implicit human contract is still there, and it is easy to accidentally break, especially in languages like PHP. PHP gives us a lot of rope, so it is even more important to be aware of the human-level contracts in our designs. My example was meant to illustrate why relying only on the language to enforce contracts isn’t always enough.

Also, regarding contracts and access: PHP is unusual in that it allows sibling classes to access each other’s protected members, which blurs boundaries you would see in stricter languages. That is exactly why I think it is worth talking about the intent and semantics behind protected, not just what is technically enforced.

So, while the type system defines what is permitted, it doesn’t always define what is sensible. This is where human reasoning and design principles like LSP come in.

— Rob

On Wednesday 06 August 2025 13:41:14 (+02:00), Rob Landers wrote:

> > > On Wed, Aug 6, 2025, at 13:26, Hans Krentel wrote:
> > > > > > > > On Sunday 03 August 2025 11:30:13 (+02:00), Rob Landers wrote:
> > > > > I'm not sure that this is a bug. You can redeclare the same type and add > > hooks (or change them), which breaks all assumptions about > > substitutability.
> > > > Rob, maybe you can lecture me a bit: Isn't substitutability on the public > > interface / protocol only? What am I not seeing in your example?
> > From a child class's perspective, the "public interface" is both protected + public of the parent. If you change or misuse a parent's implementation or invariants, it violates LSP, even if it doesn't affect external clients immediately.

Ah okay, that part is not in my book, this explains to me why in your example it violates substitutability for you, and with that thinking it also prevents or degrades implementability for me so to say, as otherwise I could not make use of visibility in classes - it would take away that tool from me or I would not treat it well, potentially leading to defects in the program.

>
> Take for example:
> > class Fruit {
> public string $kind { get => "fruit" }
> }
> > class Vegetable extends Fruit {
> public string $kind { get => "vegetable" }
> }
> > function foo(Fruit $fruit) {}
> > foo(new Vegetable); // hmmm
> > This is a "soft" violation that only makes sense to us humans, but PHP allows it. It requires us humans to realize we are performing an LSP violation and refactor the code so that we don't pass a Carrot to someone expecting a Mango.

Thankfully in this example it is all public, but I definitely would say this is not an LSP violation, just saying.

>
> This can be done through protected means as well (simply replace the properties above as protected properties used internally), and it won't be as obvious to consumers outside the class, but still there, nonetheless.
>

Okay, this is it probably just like above (for me): When $kind would be protected, it would not be part of the public protocol, and the substitutability test with the PHP runtime would still pass for the foo(Fruit) event with a Vegetable that is a Fruit (extends). That would be a test for substitutability, per the PHP runtime guarantees (it returns successfully after sending the message), it does not break the program:

>> an object (such as a class) may be replaced by a sub-object (such as a class that extends the first class) without breaking the program. (WP LSP) <<

Still trying to learn more, though.

Let me guess: The following hierarchy is not substitutable for you, as we can still pass Vegetable for Fruit on foo()'s protocol. Is that correct?

     class Fruit {
         // intentionally left blank
     }

     class Vegetable extends Fruit {
         // intentionally left blank
     }

     function foo(Fruit $fruit) {}

     foo(new Vegetable); // hmmm

-- hakre

On Thu, Aug 7, 2025, at 00:38, Hans Krentel wrote:

On Wednesday 06 August 2025 13:41:14 (+02:00), Rob Landers wrote:

On Wed, Aug 6, 2025, at 13:26, Hans Krentel wrote:

On Sunday 03 August 2025 11:30:13 (+02:00), Rob Landers wrote:

I’m not sure that this is a bug. You can redeclare the same type and
add
hooks (or change them), which breaks all assumptions about
substitutability.

Rob, maybe you can lecture me a bit: Isn’t substitutability on the
public
interface / protocol only? What am I not seeing in your example?

From a child class’s perspective, the “public interface” is both
protected + public of the parent. If you change or misuse a parent’s
implementation or invariants, it violates LSP, even if it doesn’t affect
external clients immediately.

Ah okay, that part is not in my book, this explains to me why in your
example it violates substitutability for you, and with that thinking it
also prevents or degrades implementability for me so to say, as otherwise I
could not make use of visibility in classes - it would take away that tool
from me or I would not treat it well, potentially leading to defects in the
program.

See my reply to Jonathan. But you are free to dismiss LSP when needed. There are a lot of times when LSP isn’t the right design constraint (which I briefly mention in that email), for example, during larger migration/refactors, specialized proxies, caching results, etc., or even using sibling classes as friend classes.

PHP doesn’t strictly enforce LSP everywhere. It will get out of your way when you need it to. It’s your code, you can do whatever you want with it.

Even when I see an LSP violation at work (rare, but it happens), I don’t point it out as such, but instead point out why the approach is a bad idea (maintainability, principle of least surprise, etc). If they do it continuously, then I might have to invest in some coaching for the dev, but mostly, people don’t violate LSP for the more obvious reasons, and when they do, they usually have good reasons (see above).

Take for example:

class Fruit {
public string $kind { get => “fruit” }
}

class Vegetable extends Fruit {
public string $kind { get => “vegetable” }
}

function foo(Fruit $fruit) {}

foo(new Vegetable); // hmmm

This is a “soft” violation that only makes sense to us humans, but PHP
allows it. It requires us humans to realize we are performing an LSP
violation and refactor the code so that we don’t pass a Carrot to someone
expecting a Mango.

Thankfully in this example it is all public, but I definitely would say
this is not an LSP violation, just saying.

This can be done through protected means as well (simply replace the
properties above as protected properties used internally), and it won’t be
as obvious to consumers outside the class, but still there, nonetheless.

Okay, this is it probably just like above (for me): When $kind would be
protected, it would not be part of the public protocol, and the
substitutability test with the PHP runtime would still pass for the
foo(Fruit) event with a Vegetable that is a Fruit (extends). That would be
a test for substitutability, per the PHP runtime guarantees (it returns
successfully after sending the message), it does not break the program:

an object (such as a class) may be replaced by a sub-object (such as a
class that extends the first class) without breaking the program. (WP LSP)
<<

Still trying to learn more, though.

Let me guess: The following hierarchy is not substitutable for you, as we
can still pass Vegetable for Fruit on foo()'s protocol. Is that correct?

class Fruit {
// intentionally left blank
}

class Vegetable extends Fruit {
// intentionally left blank
}

function foo(Fruit $fruit) {}

foo(new Vegetable); // hmmm

– hakre

I don’t see any reason why this example would violate LSP. There is no discernible difference between the two classes. LSP only says they are substituable in regards to type, and behaviour, and an empty class is probably only a sentinel value, in which case the behaviour is external to the type. I might have an issue with saying a vegetable is a fruit, but that is a naming issue… and naming is hard.

— Rob

On Thursday 07 August 2025 08:46:26 (+02:00), Rob Landers wrote:

On Thu, Aug 7, 2025, at 00:38, Hans Krentel wrote:
>
>
>
> On Wednesday 06 August 2025 13:41:14 (+02:00), Rob Landers wrote:
>
> >
> >
> > On Wed, Aug 6, 2025, at 13:26, Hans Krentel wrote:
> > >
> > >
> > >
> > > On Sunday 03 August 2025 11:30:13 (+02:00), Rob Landers wrote:
> > >
> > > > I'm not sure that this is a bug. You can redeclare the same type and
> add
> > > hooks (or change them), which breaks all assumptions about
> > > substitutability.
> > >
> > > Rob, maybe you can lecture me a bit: Isn't substitutability on the
> public
> > > interface / protocol only? What am I not seeing in your example?
> >
> > From a child class's perspective, the "public interface" is both
> protected + public of the parent. If you change or misuse a parent's
> implementation or invariants, it violates LSP, even if it doesn't affect
> external clients immediately.
>
> Ah okay, that part is not in my book, this explains to me why in your
> example it violates substitutability for you, and with that thinking it
> also prevents or degrades implementability for me so to say, as otherwise I
> could not make use of visibility in classes - it would take away that tool
> from me or I would not treat it well, potentially leading to defects in the
> program.

See my reply to Jonathan. But you are free to dismiss LSP when needed. There are a lot of times when LSP isn’t the right design constraint (which I briefly mention in that email), for example, during larger migration/refactors, specialized proxies, caching results, etc., or even using sibling classes as friend classes.

PHP doesn’t strictly enforce LSP everywhere. It will get out of your way when you need it to. It’s your code, you can do whatever you want with it.

Even when I see an LSP violation at work (rare, but it happens), I don’t point it out as such, but instead point out why the approach is a bad idea (maintainability, principle of least surprise, etc). If they do it continuously, then I might have to invest in some coaching for the dev, but mostly, people don’t violate LSP for the more obvious reasons, and when they do, they usually have good reasons (see above).

Thanks a lot for sharing your thoughts that openly and briefly, much appreceated.

This all sounds rather sane to me, given PHP is not a design by contract language and what makes LSP stand out from formal type theory can get easily lost then, especially when applied with force.

I've also studied your other reply to Jonathan.

>
> >
> > Take for example:
> >
> > class Fruit {
> > public string $kind { get => "fruit" }
> > }
> >
> > class Vegetable extends Fruit {
> > public string $kind { get => "vegetable" }
> > }
> >
> > function foo(Fruit $fruit) {}
> >
> > foo(new Vegetable); // hmmm
> >
> > This is a "soft" violation that only makes sense to us humans, but PHP
> allows it. It requires us humans to realize we are performing an LSP
> violation and refactor the code so that we don't pass a Carrot to someone
> expecting a Mango.
>
> Thankfully in this example it is all public, but I definitely would say
> this is not an LSP violation, just saying.
>
> >
> > This can be done through protected means as well (simply replace the
> properties above as protected properties used internally), and it won't be
> as obvious to consumers outside the class, but still there, nonetheless.
> >
>
> Okay, this is it probably just like above (for me): When $kind would be
> protected, it would not be part of the public protocol, and the
> substitutability test with the PHP runtime would still pass for the
> foo(Fruit) event with a Vegetable that is a Fruit (extends). That would be
> a test for substitutability, per the PHP runtime guarantees (it returns
> successfully after sending the message), it does not break the program:
>
> >> an object (such as a class) may be replaced by a sub-object (such as a
> class that extends the first class) without breaking the program. (WP LSP)
> <<
>
> Still trying to learn more, though.
>
> Let me guess: The following hierarchy is not substitutable for you, as we
> can still pass Vegetable for Fruit on foo()'s protocol. Is that correct?
>
>
> class Fruit {
> // intentionally left blank
> }
>
> class Vegetable extends Fruit {
> // intentionally left blank
> }
>
> function foo(Fruit $fruit) {}
>
> foo(new Vegetable); // hmmm
>
>
> -- hakre

I don’t see any reason why this example would violate LSP. There is no discernible difference between the two classes. LSP only says they are substituable in regards to type, and behaviour, and an empty class is probably only a sentinel value, in which case the behaviour is external to the type. I might have an issue with saying a vegetable is a fruit, but that is a naming issue… and naming is hard.

So by protocol, this settled a bit more in harmony. Fine.

And while naming is hard, names are harder: Given that LSP, PHP and the (earlier with hooks) class definitions are all human made, and furthermore that fruits and vegetables grow from nature, there is or was at least one local jurisdiction that would have needed them in their program exactly that way: a vegetable declared Fruit. A delicious example of sub-typing refreshingly POLArizing.

Thanks Rob.

-- hakre