Re: [PHP-DEV] Protected inheritance hierarchies

On Thu, Aug 7, 2025, at 20:37, Jonathan Vollebregt wrote:

On 8/7/25 12:38 AM, Hans Krentel wrote:

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

On 2025-08-08 10:01, Rob Landers wrote:

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.

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: Online PHP editor | output for LmLLu
protected static method: Online PHP editor | output for vjllN

Test cases currently in failure:

protected property: https://3v4l.org/br0tj
protected static property: Online PHP editor | output for 2Ue97
protected constant: https://3v4l.org/jXm6U

—Claude

On Friday 08 August 2025 09:13:09 (+02:00), Claude Pache wrote:

> 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

On Friday 08 August 2025 00:49:27 (+02:00), Morgan wrote:

On 2025-08-08 10:01, Rob Landers wrote:
> 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

On Fri, Aug 8, 2025, at 15:11, Hans Krentel wrote:

On Friday 08 August 2025 00:49:27 (+02:00), Morgan wrote:

On 2025-08-08 10:01, Rob Landers wrote:

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