Bug #73987 [Com]: Method compatibility check looks to original definition and not parent
| From: | mail at pmmaga dot net | Date: | Fri, 27 Jan 2017 09:13:13 +0000 |
| Subject: | Bug #73987 [Com]: Method compatibility check looks to original definition and not parent | ||
| References: | 1 | Groups: | php.bugs |
| Request: | Send a blank email to php-bugs+get-206975@lists.php.net to get a copy of this message | ||
Edit report at https://bugs.php.net/bug.php?id=73987&edit=1
ID: 73987
Comment by: mail at pmmaga dot net
Reported by: requinix@php.net
Summary: Method compatibility check looks to original
definition and not parent
Status: Open
Type: Bug
Package: Class/Object related
PHP Version: 7.1.1
Block user comment: N
Private report: N
New Comment:
I had a go at fixing this issue. Please review the PR. Thanks
Previous Comments:
------------------------------------------------------------------------
[2017-01-24 14:29:36] requinix@php.net
Related To: Bug #73985
------------------------------------------------------------------------
[2017-01-24 14:24:53] 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).
------------------------------------------------------------------------
--
Edit this bug report at https://bugs.php.net/bug.php?id=73987&edit=1