Search code examples
phptypestypeerrorcovariancephp-8

How can I use the 'static' type annotation for method parameters in PHP (or achieve an equivalent effect)?


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.

Reproducible 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).


Solution

  • 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 or static|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 class A is expected. However, in this example passing B instead of A will throw a TypeError, because B::test() does not accept a A 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.