-
-
Save hack3p/78eeca8f50d3297bf25768a9c72cafeb to your computer and use it in GitHub Desktop.
<?php | |
// example 1: return type | |
class A | |
{ | |
function foo(): A | |
{ | |
return $this; | |
} | |
} | |
class B extends A | |
{ | |
function foo(): B | |
{ | |
return $this; | |
} | |
} | |
// error: PHP Fatal error: Declaration of B::foo(): B must be compatible with A::foo(): A in ... | |
// example 2: arguments type hinting | |
class X {} | |
class Y extends X {} | |
class A | |
{ | |
function foo(X $bar) {} | |
} | |
class B extends A | |
{ | |
function foo(Y $bar) {} | |
} | |
// error: PHP Warning: Declaration of B::foo(Y $bar) should be compatible with A::foo(X $bar) in ... |
Your second example is wrong: class B must be usable wherever class A is usable, so must accept any value that class A accepts. If your code failed, you would not be able to write the following:
function test(A $x, A $y) {
$x->foo($y);
}
test( new B, new A );
Because it would run (new B)->foo(new A)
, which your constraint disallowed.
You could instead, if the language allowed, have a wider constraint on the sub-class:
class A
{
function foo(B $bar) {}
}
class B extends A
{
function foo(A $bar) {}
}
function test(A $a) {
$a->foo(new B);
}
test( new B ); // OK, because B::foo accept B objects as a sub-set of A objects
Or a less circular example:
class X {}
class Y extends X {}
class A
{
function foo(Y $bar) {}
}
class B extends A
{
function foo(X $bar) {}
}
function test(A $a) {
$a->foo(new Y);
}
test( new B ); // OK, because B::foo accept Y objects as a sub-set of X objects
Hi @IMSoP, thanks for responding. You right, the second example is wrong. Ofcourse i'm implied next code:
class X {}
class Y extends X {}
class A
{
function foo(X $bar) {}
}
class B extends A
{
function foo(Y $bar) {}
}
PHP Warning: Declaration of B::foo(Y $bar) should be compatible with A::foo(X $bar) in ...
Updated second example in gist.
Possibly it would be more real-world example:
interface BarInterface {}
class Bar implements BarInterface {}
interface FooInterface
{
function baz(BarInterface $bar);
}
class Foo implements FooInterface
{
function baz(Bar $bar)
{
}
}
(new Foo)->baz(new Bar);
PHP Fatal error: Declaration of Foo::baz(Bar $bar) must be compatible with FooInterface::baz(BarInterface $bar) in ...
Hi, you still have the second example wrong I'm afraid - it's still narrowing the typehint when it should only be allowed to widen it.
Consider a different sub-class, Z
, which also extends X
:
class X {}
class Y extends X {}
class Z extends X {}
class A
{
function foo(X $bar) {}
}
class B extends A
{
function foo(Y $bar) {}
}
function test(A $a) {
$a->foo(new Z);
}
test( new A ); // OK: Calls A::foo( new Z ), and Z instanceof X passes
test( new B ); // ERROR! Calls B::foo( new Z ), but Z instanceof Y fails
@ilya-k-v You've made the same mistake:
// This function should work no matter which implementation of each interface is passed in
function test( FooInterface $someFoo, BarInterface $someBar ) {
$someFoo->baz( $someBar );
}
// But it doesnt!
class DifferentBar implements BarInterface {}
test( new Foo, new DifferentBar ); // ERROR: Tries to pass DifferentBar to an implementation that only accepts Bar objects
This seems to be something people struggle to intuitively grasp, and I've been playing around with representing it graphically. You may or may not find my attempt with ASCII-art boxes enlightening: https://github.com/IMSoP/variance-pipes
@IMSoP nice graphics :)
With LSP in mind of course you're right.
It's test example for changing the variability of php types: https://bugs.php.net/bug.php?id=75823