rfc:static_return_type

PHP RFC: Static return type

Introduction

The static special class name in PHP refers to the class a method was actually called on, even if the method is inherited. This is known as “late static binding” (LSB). This RFC proposes to make static also usable as a return type (next to the already usable self and parent types).

There are a number of typical use-cases where static return types appear (currently in the form of @return static).

One are named constructors:

class Test {
    public function createFromWhatever($whatever): static {
        return new static($whatever);
    }
}

Here we want to specify that XXX::createFromWhatever() will always create an instance of XXX, not of some parent class.

Another are withXXX() style interfaces for mutating immutable objects:

class Test {
    public function withWhatever($whatever): static {
        $clone = clone $this;
        $clone->whatever = $whatever;
        return $clone;
    }
}

Here we want to specify that $foobar->withWhatever() will return a new object of class get_class($foobar), not of some parent class.

Finally, the likely most common use case are fluent methods:

class Test {
    public function doWhatever(): static {
        // Do whatever.
        return $this;
    }
}

Here we actually have a stronger contract than in the previous two cases, in that we require not just an object of the same class to be returned, but exactly the same object. However, from the type system perspective, the important property we need is that the return value is an instance of the same class, not a parent class.

Proposal

Allowed positions

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.

For property types, we have the additional problem that the static type conflicts with the static modifier:

class A {
    // Is this an untyped static property,
    // or an instance property of type static?
    public static $a;
}

For this reason, we disallow static types in properties/parameters already at the grammar level, rather than emitting a nicer error message in the compiler.

Variance and Subtyping

For the purpose of variance checks, static is considered a subtype of self. That is, the following inheritance is legal:

class A {
    public function test(): self {}
}
class B extends A {
    public function test(): static {}
}
class C extends B {}

When considering just class B, replacing a self type with a static type results in identical behavior. However, the return value of C::test() is further restricted relative to a self type. For this reason static is considered a subtype of self.

The converse replacement shown in the following is not legal:

class A {
    public function test(): static {}
}
class B extends A {
    public function test(): self {}
}
class C extends B {
    // To spell out the inherited signature:
    public function test(): B {}
}

In this case, the effective return type of C::test() is B, even though the original type on A::test() would have required it to be C. This violates covariance/LSP.

It should be noted that self here refers to the resolved type of the current class, it does not have to be spelled as self in particular. For example, the following is also legal:

class A {
    public function test(): A {}
}
class B extends A {}
class C extends B {
    public function test(): static {}
}

Here, self is C, which is a subtype of A, making the replacement with static legal.

Reflection

While internally the static type is treated as a special builtin type, it will be reported as a class type in reflection, for symmetry with self and parent.

class Test {
    public function method(): static {}
}
 
$rm = new ReflectionMethod(Test::class, 'method');
$rt = $rm->getReturnType();
var_dump($rt->isBuiltin()); // false
var_dump($rt->getName()); // "static"

Backward Incompatible Changes

There are no backwards incompatible changes in this proposal.

Future Scope

For the fluent method example above, many projects will use a @return $this annotation, rather than @return static. We could in principle also support this syntax natively:

class Test {
    public function doWhatever(): $this {
        // Do whatever.
        return $this;
    }
}

However, $this is not a real type, and it is unclear what the advantage of specifying $this rather than static would be from a type system level perspective.

Vote

Yes/No.

rfc/static_return_type.txt · Last modified: 2020/01/08 23:30 by tandre