Currently PHP has mostly invariant parameter types and mostly invariant return types. This means that if a method of a super-type has a parameter or return type constraint of T
then the corresponding parameter or return type constraint of method of the sub-type must also be T
. However, PHP does special case a few things such as removing a type constraint on a parameter or adding one where one didn't exist for a return type; here is an example of both cases:
interface A { function m(B $z); } interface B extends A { // permitted function m($z): A; }
However, PHP does not permit choosing less specific parameter types or more specific return types even though these substitutions are type-safe:
interface X { function m(Y $z): X; } interface Y extends X { // not permitted but type-safe function m(X $z): Y; }
This RFC aims to support these additional cases.
When determining the compatibility of an method with its parent's the engine should now permit less specific parameter types and more specific return types as long as the new types still accept the types specified by the parents. In other words: a parameter type can be substituted for one of its super-types and a return type can substitute a sub-type.
This RFC will also fix Bug #76451: Aliases during inheritance type checks affected by opcache.
This RFC adds variance support to the object
type. Variance on the object type was previously rejected when the object
type was added, presumably on the grounds it should be added when variance is made more general, which is the purpose of this RFC.
This RFC does not add variance support to the callable
type. Anyone desiring improved callable
type support should create a different RFC.
This RFC also expands support for compile-time resolving of the “parent” pseudo-type. The existing engine does this in some places, but not extensively. This work is included in the RFC because it made the code for the RFC a bit easier, but if necessary this work can be split out.
Covariant return type with object
:
interface Factory { function make(): object; } class UserFactory implements Factory { function make(): User; }
Contravariant parameter type with iterable
:
interface Concatable { function concat(Iterator $input); } class Collection implements Concatable { // accepts all iterables, not just Iterator function concat(iterable $input) {/* . . . */} }
Refining an iterator returned by IteratorAggregate::getIterator
:
interface QueueIterator extends Iterator { /*...*/ } interface Queue extends IteratorAggregate { /*...*/ function getIterator(): QueueIterator; } final class ArrayQueueIterator implements QueueIterator { /*...*/ } final class ArrayQueue implements Queue { /*...*/ function getIterator(): ArrayQueueIterator {} }
Consider the following code, all of which is defined in the same file:
interface Collection extends Countable, IteratorAggregate { function getIterator(): Iterator; } class Vector implements Collection { function getIterator(): VectorIterator { /* . . . */ } } class VectorIterator implements Iterator { /* . . . */ }
All three types will need runtime variance checks because they extend or implement another type. To prevent issues with autoloading and order-of-definition issues, the implementation delays the variance verification until after the last consecutive type declaration. Here is what the relevant opcodes looked like from a preliminary implementation (output from phpdbg
):
L1-13 {main}() /tmp/rfc_autoload.php - 0x7f6b49a89000 + 7 ops L2 #0 DECLARE_CLASS "collection" L6 #1 DECLARE_CLASS "vector" L10 #2 DECLARE_CLASS "vectoriterator" L12 #3 VERIFY_VARIANCE "collection" L12 #4 VERIFY_VARIANCE "vector" L12 #5 VERIFY_VARIANCE "vectoriterator" L13 #6 RETURN<-1> 1
Note that all 3 types are declared, then all 3 are verified in the same order they were written in.
At the moment the implementation will tolerate consecutive class and function definitions.
Whether a parameter is passed by-reference or not does not impact the variance. This is because the type checks are moment-in-time checks, not lifetime checks. For example, this is valid:
function array_baz(array &$in) { $in = 1; } $array = []; array_baz($array); // valid var_export($array); // 1
Therefore, by-reference parameters are still contravariant, and by-reference returns are still covariant. It is possible that in the future we may get out
or inout
parameter types, which behave more like references to typed properties. These would probably have different variance requirements.
There are no intended incompatibilities. All incompatibilities should be reported and treated as bugs.
This patch does emit a new E_DEPRECATED
warning when the “parent” pseudo-type is used in places that do not have a parent type. In the next major version (currently 8.0) this should elevated to an E_COMPILE_ERROR
. Emitting new warnings are generally not considered backwards incompatible changes even though they technically can break things with custom error handlers. Again, this work with compile-time resolving of “parent” can be split out from the RFC if necessary.
TODO: New functions and macros should be introduced to make working with variant types in C easier.
This RFC targets PHP 7.NEXT because it does not have any known compatibility issues.
This RFC requires two-thirds of voters to select “yes” for this RFC to pass.
Voting will be open until at least January 2nd, 2019 (2019-01-02).
A preliminary implementation can be found at https://github.com/php/php-src/compare/master...morrisonlevi:variance2. This is a preliminary patch with some obvious duplication of work, so performance impact has not been measured. However, there will be some impact as this adds new opcodes to verify variance at runtime, which obviously has some cost.
Tests are included in the implementation, but more tests are always welcome. Notably, some tests should be added to deal with multiple parents with differing but compatible signatures.
A future RFC may consider other super-types for existing types:
mixed
: includes all types that exist or will ever exist including null; this matches our usage of mixed in our documentation.scalar
: includes bool, int, float, and string; this matches our is_scalar
function.numeric
: this probably needs even more discussion because is_numeric
does not deal exclusively with types: it also checks string values.
Improving callable
type support is also out of scope for this RFC and could be a good candidate for its own RFC.
After the project is implemented, this section should contain
Announcement to Internals mailinglist: