|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2017-01-24 14:24 UTC] requinix@php.net
Description: ------------ Originally spotted as bug #73985. When PHP validates method signatures for compatibility, if a method is defined in an interface then compatibility is measured against the interface and any parent method's signature is ignored. This leads to inconsistencies... Example #1: method is defined in an interface (valid) - https://3v4l.org/pblqI ---------- interface I { public function example($a, $b, $c); } class A implements I { public function example($a, $b = null, $c = null) { } // compatible with I::example } class B extends A { public function example($a, $b, $c = null) { } // compatible with I::example } Example #2: method is not defined in an interface (invalid) - https://3v4l.org/bJJ1h ---------- //interface I { // public function example($a, $b, $c); //} class A { public function example($a, $b = null, $c = null) { } } class B extends A { public function example($a, $b, $c = null) { } // not compatible with A::example } The same signature is used in A and B, however only the second has a problem. The first is easy to explain on its own ("example" was defined in I so methods must be compatible with I::example) and the second is easy to explain on its own ("example" was defined in A so methods must be compatible with A::example) however the two together are inconsistent. The problem appears when calling a function using an I or A parameter type - https://3v4l.org/OneV3 --- function accepts_i(I $i) { $i->example(1, 2, 3); } accepts_i(new B); // no problem function accepts_a(A $a) { $a->example(1); } accepts_a(new B); // problem --- PHP <7.1: missing argument 2 for B::example PHP >=7.1: ArgumentCountError: Too few arguments to function B::example, 1 passed A::example() only has one required argument, therefore it should be safe for accepts_a to call ->example(1). But it isn't. Like with method parameters, this problem also exists for return types. Example #3: interface does not have return type (valid) - https://3v4l.org/Jik7I ---------- interface I { public function example(); } class A implements I { public function example(): int { } // compatible with I::example } class B extends A { public function example(): string { } // compatible with I::example } Example #4: class has a return type (invalid) - https://3v4l.org/n1q0G ---------- <?php //interface I { // public function example(); //} class A { public function example(): int { } } class B extends A { public function example(): string { } // not compatible with A::example } Like with method parameters, this can result in unexpected behavior. Unlike with method parameters, there's no warning about it - https://3v4l.org/TUbfJ --- function accepts_i_expects_any(I $i) { var_dump($i->example()); } accepts_i_expects_any(new B); // receives string, no problem function accepts_a_expects_int(A $a) { var_dump($a->example()); } accepts_a_expects_int(new B); // receives string, problem --- Proposed solution: methods in a subclass are validated against methods in the nearest ancestor who (re)defines the method - be that normally, as abstract, or using a trait. The result is that since A implemented example(), B::example gets validated against that rather than the original definition of I::example. BC: Yes, but code reliant on current behavior is susceptible to the "inconsistencies" noted earlier so it's already flawed. Test script: --------------- <?php // https://3v4l.org/gl1Gt interface I { public function example($a, $b, $c); } class A implements I { public function example($a, $b = null, $c = null): int { } } class B extends A { public function example($a, $b, $c = null): string { } } ?> Expected result: ---------------- Error that B::example is not compatible with A::example, due to the return type (a fatal error by itself) and the second required argument (a warning by itself). Actual result: -------------- No error(s). PatchesPull Requests
Pull requests:
HistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Wed Oct 29 08:00:01 2025 UTC |
Yeah, I've understood your idea exactly as you've explained lately, to enforce strictness on the language level. As for usages in other languages, what i had in mind, here is a sample code in Java, but could be good in any other of C# or C++, etc. I.java interface I { public int method(int i); } A.java class A implements I { public int method(int i) { return 0; } } B.java class B extends A { public String method(String i) { return ""; } } A implements I, B doesn't explicitly implement it, but derives from A and additionally overloads the method. One can rephrase it in other way - "class B extends A implements I", as B already contains an instance of "method", so maybe it's even right to say it indirectly implements I, at least it ensures LSP. Clear, tihs is not possible in PHP, that's why i mentioned it right in my first sentence :) I'm not sure, what else would be required to enforce LSP on the language level in PHP, as the weak interface declaration is still a factor, for what this ticket cares. For example in PHP, one can can still do class B implements I { public function example(): string { } } so then both A und B are perfectly an instance of I, but are incompatible indeed. I'd see an explicit interface declaration as a more robust and universal way, but a mitigation might be not that easy. There are possibly other cases. Anyway, if some particular restriction helps to fix a widely possible anti pattern sub case, it still could be considered as a sensible thing to do. Probably it's a bit wider topic than just one bug ticket. Thanks.