Foo is a base class with specific methods. Most of theses methods use the same type (ex: Foo::setNext(self $foo)
).
I want to create classes, that extends Foo, and are only allowed to use the strictly same type as themself (an object of type Extend1Foo cannot be used with objects of type Extend2Foo).
In the following code, because of the return type static, getBar()
throws an error. That's what I want.
But, setBar()
allow to receive any instance of Foo as parameter, because of self
parameter type.
Reproductible example :
class Foo
{
private ?self $bar = null;
public function getBar(): static {
return $this->bar;
}
public function setBar(self $object): void {
$this->bar = $object;
}
}
class Foo1 extends Foo { /* specific methods */ }
class Foo2 extends Foo { /* specific methods */ }
$foo1 = new Foo1;
$foo1->setBar(new Foo2); // <<< No TypeError, but I want it.
$foo2 = $foo1->getBar(); // <<< Got error, I'm OK.
I've forced the TypeError :
public function setBar(self $object): void
{
if (get_class($object) != static::class) {
throw new TypeError(
sprintf('%s::%s(): Parameter value must be of type %s, %s given',
__class__, __function__,
static::class, get_class($object)
)
);
}
$this->child = $object;
}
and use :
$foo1 = new Foo1;
$foo1->setBar(new Foo2); // TypeError : Foo::setBar(): Parameter value must be of type Foo1, Foo2 given
This is the expected behavior.
My question is :
Is there a way to avoid this dynamic test on types ? I think the static
cannot be used in parameters, instead of self
, like public function setBar(static $object)
.
CodePudding user response:
Your use case was explicitly considered and rejected in the PHP RFC which added the static
type annotation in return position, because allowing it would defeat the point of inheritance:
The
static
type is only allowed inside return types, where it may also appear as part of a complex type expression, such as?static
orstatic|array
.To understand why
static
cannot be used as a parameter type (apart from the fact that this just makes little sense from a practical perspective), consider the following example:class A { public function test(static $a) {} } class B extends A {} function call_with_new_a(A $a) { $a->test(new A); } call_with_new_a(new B);
Under the Liskov substitution principle (LSP), we should be able to substitute class
B
anywhere classA
is expected. However, in this example passingB
instead ofA
will throw aTypeError
, becauseB::test()
does not accept aA
as a parameter.More generally,
static
is only sound in covariant contexts, which at present are only return types.
On the other hand, it is possible to encapsulate the interface you want in a trait, which is what PHP calls a mixin:
trait Bar {
private ?self $bar = null;
public function getBar(): static {
return $this->bar;
}
public function setBar(self $object) {
$this->bar = $object;
}
}
class Foo {}
final class Foo1 extends Foo { use Bar; }
final class Foo2 extends Foo { use Bar; }
try {
$foo1 = new Foo1;
$foo1->setBar(new Foo2); // TypeError
}
catch (Throwable $error)
{
echo $error->getMessage(), PHP_EOL;
}
Done this way, the public methods will not be part of the Foo
class interface, but that is probably for the best, since per above there is no way to ascribe a meaningful type signature to it.