PHP RFC: Static return type
- Date: 2020-01-08
- Author: Nikita Popov nikic@php.net
- Target Version: PHP 8.0
- Status: Implemented
- Implementation: https://github.com/php/php-src/pull/5062
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
.
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
Voting started 2020-01-28 and ends 2020-02-11.