I came across this edge case today:
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
I came across this edge case today:
I was just informed that my first message ended up in someone's spam
box, not sure why. I'm bumping without content in case it's happening to
more people and the link tripped something.
I came across this edge case today:
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.0I 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
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 https://3v4l.org/0SRWb#v8.4.10However, 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
Ah I see, thanks.
Both getProperties and array casting clobber the shadowed protected
properties (Unlike private properties) but you can access the property
with reflection by reflecting the parent class.
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?
I guess that would introduce either too much of a performance penalty
recursively checking access for each protected property read, or too
much complexity trying to pin down the variable type at parse time.
Thanks for the info!
I came across this edge case today:
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...<snip>...
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.10However, 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
__
I came across this edge case today:
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...<snip>...
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
I came across this edge case today:
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...<snip>...
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.10However, 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.--
AlexHi 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 came across this edge case today:
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...<snip>...
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.
--
AlexHi 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 https://github.com/Crell/Serde/issues/47#issuecomment-1890966829.
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
I came across this edge case today:
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...<snip>...
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.10However, 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.--
AlexHi 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,
AlexI 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
https://github.com/Crell/Serde/issues/47#issuecomment-1890966829.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
https://www.php.net/manual/en/language.oop5.visibility.php and fix the
redeclaration issue to make things consistent.
The other way is to deprecate sibling relations.
--
Valentin
__
__
__
I came across this edge case today:
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...<snip>...
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.
--
AlexHi 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,
AlexI 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 https://github.com/Crell/Serde/issues/47#issuecomment-1890966829.
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/1261633126687805440So one way is to explicitly mention this feature in the Visibility docs https://www.php.net/manual/en/language.oop5.visibility.php 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
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]
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
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]
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
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: https://3v4l.org/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'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.
No it doesn't, read the "Property type variance" section on the property
hooks RFC:
Normal properties are neither covariant nor contravariant; their type may not change in a subclass. The reason for that is “get” operations MUST be covariant, and “set” operations MUST be contravariant. The only way for a property to satisfy both requirements is to be invariant.
With abstract properties (on an interface or abstract class) or virtual properties, it is possible to declare a property that has only a get or set operation. As a result, abstract properties or virtual properties that have only a get operation required MAY be covariant. Similarly, an abstract property or virtual property that has only a set operation required MAY be contravariant.
Once a property has both a get and set operation, however, it is no longer covariant or contravariant for further extension. That is, it is now invariant (as all properties are in 8.3 and earlier).
Your example code does fail but this isn't because of the hooks (The
getter from A->$v is inherited in C) it's because you've redeclared it
without a default value and it's trying to access an uninitialized
property: https://3v4l.org/pv39f#v8.4.11 vs https://3v4l.org/VD6ZK#v8.4.11
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.
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
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
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.
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.
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
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
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
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
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.
Don't believe everything you read on the internet.
If you weren't allowed to change a parent's implementation, then you
wouldn't be permitted to override methods at all, at which point
inheritance is pointless.
It's funny that he's starting to talk about invariants here. Seems he
doesn't realize every example he's given so far is covariant (Besides
the one at the beginning of the thread where he confused static and
non-static properties)
He's trying to argue that LSP is about implementation when it's not.
This is an interface with a function signature:
interface I {
function f(): string;
}
See a fruit anywhere? Me neither. That's because there isn't one. It's
not part of the signature. It's not part of the interface. It's not part
of the definition. It's not part of the contract.
This is a class with the same function signature:
class C {
function f(): string {
return "fruit";
}
}
In fact, this class could implement that interface without any problems,
because the signature is identical. The types are identical. There is no
LSP violation. There is also no contract that implies C::f() returns
"fruit".
If you made that assumption I'd call it a lack of error handling from
someone who only considered the happy path. Static analysis tools exist
to stop you from making these mistakes.
Once more, just to drive the point home. This is an interface with a
function signature:
interface I {
public int $p { get; }
}
This is a class with the same function signature (And one more for good
measure, because I::$p is covariant!):
class C {
public int $p;
}
In fact, this class could implement that interface without any problems,
because the signature is identical. The types are identical. There is no
LSP violation. There is no contract that implies that getting C->p will
return a certain value, other than that the value will be an int.
Back to the original issue: I'm going to open a github issue on this
since it's clearly a bug and I don't see fixing it breaking any existing
code.
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.Don't believe everything you read on the internet.
If you weren't allowed to change a parent's implementation, then you
wouldn't be permitted to override methods at all, at which point
inheritance is pointless.It's funny that he's starting to talk about invariants here. Seems he
doesn't realize every example he's given so far is covariant (Besides
the one at the beginning of the thread where he confused static and
non-static properties)He's trying to argue that LSP is about implementation when it's not.
This is an interface with a function signature:interface I {
function f(): string;
}See a fruit anywhere? Me neither. That's because there isn't one. It's
not part of the signature. It's not part of the interface. It's not part
of the definition. It's not part of the contract.This is a class with the same function signature:
class C {
function f(): string {
return "fruit";
}
}In fact, this class could implement that interface without any problems,
because the signature is identical. The types are identical. There is no
LSP violation. There is also no contract that implies C::f() returns
"fruit".If you made that assumption I'd call it a lack of error handling from
someone who only considered the happy path. Static analysis tools exist
to stop you from making these mistakes.Once more, just to drive the point home. This is an interface with a
function signature:interface I {
public int $p { get; }
}This is a class with the same function signature (And one more for good
measure, because I::$p is covariant!):class C {
public int $p;
}In fact, this class could implement that interface without any problems,
because the signature is identical. The types are identical. There is no
LSP violation. There is no contract that implies that getting C->p will
return a certain value, other than that the value will be an int.
Back to the original issue: I'm going to open a github issue on this
since it's clearly a bug and I don't see fixing it breaking any existing
code.
Hey Jonathan:
It seems we’re talking past each other a bit, so let me clarify.
Liskov’s original work (which is worth reading, since it seems you haven’t) frames LSP as a behavioural contract, not merely a type signature. When the principle was introduced in the 1980s, modern type systems barely existed—the focus was always on whether substituting a child for a parent would break expected behaviour, not just the surface-level types.
For example, if A::foo(): int
promises to always return a non-zero integer, and a subclass overrides foo()
to only return zero, that violates the contract—even though the type signatures match perfectly. This is the sort of semantic guarantee that LSP is about, and it is discussed in nearly every reputable textbook and conference talk on OO design. Languages like PHP can’t enforce these contracts, but the principle is what helps prevent subtle design bugs and behavioural “surprises.”
You mention that overriding methods is the point of inheritance. Of course! That is where LSP matters most. It is about ensuring that overriding methods (and properties) in subclasses don’t break expectations encoded or implied in the parent.
On invariants: These are a textbook part of LSP. The values and guarantees behind properties (not just their types) are what client code may rely on. That is why invariants are so often referenced in the LSP context.
As for your example with interfaces and signatures: Yes, two signatures can match perfectly, and yet LSP can still be violated if the contract (explicit or implicit) is not honoured. Static analysis tools can help, but they only go so far—they can’t check for semantic compatibility.
So while a language or type system may permit something, that doesn’t mean it’s a good idea from a design or maintainability perspective.
Looking forward to your GitHub issue.
— Rob
For example, if |A::foo(): int| promises to always return a non-zero
integer, and a subclass overrides |foo()| to only return zero, that
violates the contract—even though the type signatures match perfectly.
This is the sort of semantic guarantee that LSP is about, and it is
discussed in nearly every reputable textbook and conference talk on OO
design. Languages like PHP can’t enforce these contracts, but the /
principle/ is what helps prevent subtle design bugs and behavioural
“surprises.”
Indeed. Those contracts only become enforceable in the type signature
when your type system is robust enough to be able to express them. If
you could declare |A::foo(): NonzeroInt| then the signature could
prevent an overriding subclass C from returning zero from that function
(while still allowing |C::foo(): PositiveInt|, say); the compiler can
verify that the type of the expression C::foo() is written to return is
of type NonzeroInt (or PositiveInt).
In the absence of being able to declare such constraints in the type
system, one has to fall back on other documentation about what behaviour
is and isn't acceptable.
For example, if |A::foo(): int| promises to always return a non-zero integer, and a subclass overrides |foo()| to only return zero, that violates the contract—even though the type signatures match perfectly. This is the sort of semantic guarantee that LSP is about, and it is discussed in nearly every reputable textbook and conference talk on OO design. Languages like PHP can’t enforce these contracts, but the / principle/ is what helps prevent subtle design bugs and behavioural “surprises.”
Indeed. Those contracts only become enforceable in the type signature when your type system is robust enough to be able to express them. If you could declare |A::foo(): NonzeroInt| then the signature could prevent an overriding subclass C from returning zero from that function (while still allowing |C::foo(): PositiveInt|, say); the compiler can verify that the type of the expression C::foo() is written to return is of type NonzeroInt (or PositiveInt).In the absence of being able to declare such constraints in the type system, one has to fall back on other documentation about what behaviour is and isn't acceptable.
That is certainly always useful, especially when it was written down, as it allows to read both, the fine-print, and between the lines. As you have just quoted Rob's suggestion while replying to it, allow me the opportunity to highlight a key sentence for me under the pretext of the earlier quote, and the much appreciated association with documentation of yours to illustrate that:
[...] the / principle/ [here, LSP,] is what helps prevent subtle design bugs and behavioural “surprises.”
Documentation, e.g. of pre- and postconditions, loop (in)variants, class invariants, etc, we can already make use of those corner-stones of the LSP to reason about sub-types, the LSP can inspire us of what or how we document. And that's what I've learned these days from Rob: Without reasoning about the LSP. I knew already for what I love Liskov for, but the LSP is so much a loaded thing in discussions, I had to get a couple of things out of the way first to only understand Robs reasoning. I came here by the error message, and was looking for what I was missing with it.
"Oh the types in PHP must do that for us, otherwise my scripts are doomed."
No.
The compiler can only handle the type but not the sub-type, because of the halting problem.
And while my programs can be doomed because of the halting problem (4. Every program is a part of some other program and rarely fits.), I as human am still able to reason about the correctness of my program.
Just my 2 cents
-- hakre
For example, if |A::foo(): int| promises to always return a non-zero integer, and a subclass overrides |foo()| to only return zero, that violates the contract—even though the type signatures match perfectly. This is the sort of semantic guarantee that LSP is about, and it is discussed in nearly every reputable textbook and conference talk on OO design. Languages like PHP can’t enforce these contracts, but the / principle/ is what helps prevent subtle design bugs and behavioural “surprises.”
Indeed. Those contracts only become enforceable in the type signature when your type system is robust enough to be able to express them. If you could declare |A::foo(): NonzeroInt| then the signature could prevent an overriding subclass C from returning zero from that function (while still allowing |C::foo(): PositiveInt|, say); the compiler can verify that the type of the expression C::foo() is written to return is of type NonzeroInt (or PositiveInt).In the absence of being able to declare such constraints in the type system, one has to fall back on other documentation about what behaviour is and isn't acceptable.
That is certainly always useful, especially when it was written down, as it allows to read both, the fine-print, and between the lines. As you have just quoted Rob's suggestion while replying to it, allow me the opportunity to highlight a key sentence for me under the pretext of the earlier quote, and the much appreciated association with documentation of yours to illustrate that:
[...] the / principle/ [here, LSP,] is what helps prevent subtle design bugs and behavioural “surprises.”
Documentation, e.g. of pre- and postconditions, loop (in)variants, class invariants, etc, we can already make use of those corner-stones of the LSP to reason about sub-types, the LSP can inspire us of what or how we document. And that's what I've learned these days from Rob: Without reasoning about the LSP. I knew already for what I love Liskov for, but the LSP is so much a loaded thing in discussions, I had to get a couple of things out of the way first to only understand Robs reasoning. I came here by the error message, and was looking for what I was missing with it.
"Oh the types in PHP must do that for us, otherwise my scripts are doomed."
No.
The compiler can only handle the type but not the sub-type, because of the halting problem.
And while my programs can be doomed because of the halting problem (4. Every program is a part of some other program and rarely fits.), I as human am still able to reason about the correctness of my program.
Just my 2 cents
-- hakre
To add to this, here’s one of my favorite examples, ironically, one often used in beginner OOP tutorials:
class Rectangle {
public int $width;
public int $height;
}
class Square extends Rectangle {
public int $width { get => $this->width; set => $this->width = $this->height = $value; }
public int $height { get => $this->height; set => $this->height = $this->width = $value; }
}
This is a classic LSP violation. Square changes Rectangle's contract by linking width/height, removing the independence that Rectangle promised. Meaning when we pass it as a Rectangle and we try to make the Square full screen, it will either be too short length (set width first) or too tall (set height first). In other words, a "Square is a Rectangle" violates LSP, in practice. Yet, this is quite often taught as a beginner example to OOP.
Are there cases where you'd want to do this deliberately? Yes, probably. Would you be surprised to find a rectangle extended to the edge of the screen did not extend to the edge of the screen? Yes, probably. In fact, it would probably be filed as a bug.
Interestingly, your product people will tell you to "stretch" it (or perform some other transform) making a Square behave like a Rectangle again. You'd probably end up with something more akin to this:
class Rectangle {
public int $width;
public int $height;
public static function asSquare(int $widthAndHeight): static { /* set width and height */ }
}
Anyway, I digress. Software design is fun... it's a great time to be alive.
— Rob
Le 7 août 2025 à 20:37, Jonathan Vollebregt jnv@jnvsor.net a écrit :
Back to the original issue: I'm going to open a github issue on this since it's clearly a bug and I don't see fixing it breaking any existing code.
Hi,
Sorry to reply without completely reading this long thread.
As many of us understand it, the application of Liskov Substitution Principle indeed shows that, if you can access a member declared in a given class from a given context, you ought also be able to access the same member when it is redeclared in a subclass.
This is true whether the member is concretly a method, a property or a constant, or whether it is static or not.
I am writing this to draw the attention that the issue should be resolved not only for protected properties, but also for protected constants.
Test cases currently passed:
protected method: https://3v4l.org/LmLLu
protected static method: https://3v4l.org/vjllN
Test cases currently in failure:
protected property: https://3v4l.org/br0tj
protected static property: https://3v4l.org/2Ue97
protected constant: https://3v4l.org/jXm6U
—Claude
Le 7 août 2025 à 20:37, Jonathan Vollebregt jnv@jnvsor.net a écrit :
Back to the original issue: I'm going to open a github issue on this since it's clearly a bug and I don't see fixing it breaking any existing code.
Hi,
Sorry to reply without completely reading this long thread.
As many of us understand it, the application of Liskov Substitution Principle indeed shows that, if you can access a member declared in a given class from a given context, you ought also be able to access the same member when it is redeclared in a subclass.
That could be, and if it is of interest, from what I remember about Liskov when she was talking about the LSP, she would not declare that property in the (base) type.
Doing as she would, does not only solve what you describe as the issue at hand, it has also helped me in the past to benefit from the LSP when scripting with the PHP language.
I'd also like to remind that the expressive capabilities of the language are rather limited for class invariants and the history rule, adding an escape hatch because users need it can be absolutely legit, but we should be clear about that this will weaken them further for the foreseeable time.
-- hakre
I’m not sure what you mean? https://3v4l.org/WKILh#v8.4.10
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: https://3v4l.org/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: https://3v4l.org/S7GmM
You can see the same result with var_dump (and serialize, and
debug_zval_dump, ...): https://3v4l.org/FqecC
I think the relevant code is this, in zend_declare_typed_property:
https://heap.space/xref/php-src/Zend/zend_API.c?r=78d96e94fa8e05dd59d03aa4891fa843ebc93ef8#4661
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]
This example seems more to the point:
I didn't notice the static in Rob's example. Alex/Rowan are right,
instance properties are definitely not shadowed.
Anyway that brings back my original question: Can we fix/relax this?
I don't see any need to prevent accessing properties that were declared
on a parent. Should I just submit a bug report on github?