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