Skip to content

Commit 8f8fcbb

Browse files
committed
Support full variance if autoloading is used
Keep track of delayed variance obligations and check them after linking a class is otherwise finished. Obligations may either be unresolved method compatibility (because the necessecary classes aren't available yet) or open parent/interface dependencies. The latter occur because we allow the use of not fully linked classes as parents/interfaces now. An important aspect of the implementation is we do not require classes involved in variance checks to be fully linked in order for the class to be fully linked. Because the involved types do have to exist in the class table (as partially linked classes) and we do check these for correct variance, we have the guarantee that either those classes will successfully link lateron or generate an error, but there is no way to actually use them until that point and as such no possibility of violating the variance contract. This is important because it ensures that a class declaration always either errors or will produce an immediately usable class afterwards -- there are no cases where the finalization of the class declaration has to be delayed until a later time, as earlier variants of this patch did. Because variance checks deal with classes in various stages of linking, we need to use a special instanceof implementation that supports this, and also introduce finer-grained flags that tell us which parts have been linked already and which haven't. Class autoloading for variance checks is delayed into a separate stage after the class is otherwise linked and before delayed variance obligations are processed. This separation is needed to handle cases like A extends B extends C, where B is the autoload root, but C is required to check variance. This could end up loading C while the class structure of B is in an inconsistent state.
1 parent 89b2d88 commit 8f8fcbb

23 files changed

+762
-69
lines changed

UPGRADING

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ PHP 7.4 UPGRADE NOTES
163163
public function method(): B {}
164164
}
165165

166-
This feature is currently restricted to non-cyclic type references only.
166+
Full variance support is only available if autoloading is used. Inside a
167+
single file only non-cyclic type references are possible, because all
168+
classes need to be available before they are referenced.
167169
RFC: https://wiki.php.net/rfc/covariant-returns-and-contravariant-parameters
168170

169171
. Added support for coalesce assign (??=) operator. For example:

Zend/tests/bug30922.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ var_dump($a instanceOf A);
1010
echo "ok\n";
1111
?>
1212
--EXPECTF--
13-
Fatal error: Interface 'RecurisiveFooFar' not found in %sbug30922.php on line %d
13+
Fatal error: Interface RecurisiveFooFar cannot implement itself in %s on line %d
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
Class order allowed with autoloading (1)
3+
--FILE--
4+
<?php
5+
6+
spl_autoload_register(function($class) {
7+
if ($class === 'A') {
8+
class A {
9+
public function method() : B {}
10+
}
11+
var_dump(new A);
12+
} else if ($class == 'B') {
13+
class B extends A {
14+
public function method() : C {}
15+
}
16+
var_dump(new B);
17+
} else {
18+
class C extends B {
19+
}
20+
var_dump(new C);
21+
}
22+
});
23+
24+
var_dump(new C);
25+
26+
?>
27+
===DONE===
28+
--EXPECT--
29+
object(A)#2 (0) {
30+
}
31+
object(B)#2 (0) {
32+
}
33+
object(C)#2 (0) {
34+
}
35+
object(C)#2 (0) {
36+
}
37+
===DONE===
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
Class order allowed with autoloading (2)
3+
--FILE--
4+
<?php
5+
6+
spl_autoload_register(function($class) {
7+
if ($class === 'A') {
8+
class A {
9+
public function method() : B {}
10+
}
11+
var_dump(new A);
12+
} else if ($class == 'B') {
13+
class B extends A {
14+
public function method() : C {}
15+
}
16+
var_dump(new B);
17+
} else {
18+
class C extends B {
19+
}
20+
var_dump(new C);
21+
}
22+
});
23+
24+
// Same as autoload1 test case, but with a different autoloading root
25+
var_dump(new B);
26+
27+
?>
28+
===DONE===
29+
--EXPECT--
30+
object(A)#2 (0) {
31+
}
32+
object(C)#2 (0) {
33+
}
34+
object(B)#2 (0) {
35+
}
36+
object(B)#2 (0) {
37+
}
38+
===DONE===
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
Class order allowed with autoloading (3)
3+
--FILE--
4+
<?php
5+
6+
spl_autoload_register(function($class) {
7+
if ($class == 'A') {
8+
class A {
9+
public function method(): X {}
10+
}
11+
var_dump(new A);
12+
} else if ($class == 'B') {
13+
class B extends A {
14+
public function method(): Y {}
15+
}
16+
var_dump(new B);
17+
} else if ($class == 'X') {
18+
class X {
19+
public function method(): A {}
20+
}
21+
var_dump(new X);
22+
} else if ($class == 'Y') {
23+
class Y extends X {
24+
public function method(): B {}
25+
}
26+
var_dump(new Y);
27+
}
28+
});
29+
30+
var_dump(new B);
31+
32+
?>
33+
===DONE===
34+
--EXPECT--
35+
object(A)#2 (0) {
36+
}
37+
object(X)#2 (0) {
38+
}
39+
object(Y)#2 (0) {
40+
}
41+
object(B)#2 (0) {
42+
}
43+
object(B)#2 (0) {
44+
}
45+
===DONE===
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
Class order allowed with autoloading (4)
3+
--FILE--
4+
<?php
5+
6+
// Same as autoload3 test case, but with X, Y being interfaces.
7+
spl_autoload_register(function($class) {
8+
if ($class == 'A') {
9+
class A {
10+
public function method(): X {}
11+
}
12+
var_dump(new A);
13+
} else if ($class == 'B') {
14+
class B extends A {
15+
public function method(): Y {}
16+
}
17+
var_dump(new B);
18+
} else if ($class == 'X') {
19+
interface X {
20+
public function method(): A;
21+
}
22+
var_dump(interface_exists('X'));
23+
} else if ($class == 'Y') {
24+
interface Y extends X {
25+
public function method(): B;
26+
}
27+
var_dump(interface_exists('Y'));
28+
}
29+
});
30+
31+
var_dump(new B);
32+
33+
?>
34+
===DONE===
35+
--EXPECT--
36+
object(A)#2 (0) {
37+
}
38+
bool(true)
39+
bool(true)
40+
object(B)#2 (0) {
41+
}
42+
object(B)#2 (0) {
43+
}
44+
===DONE===
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
--TEST--
2+
Class order allowed with autoloading (5)
3+
--FILE--
4+
<?php
5+
6+
// Similar to variance3, but one more class hierarchy in the cycle
7+
spl_autoload_register(function($class) {
8+
if ($class == 'A') {
9+
class A {
10+
public function method(): X {}
11+
}
12+
var_dump(new A);
13+
} else if ($class == 'B') {
14+
class B extends A {
15+
public function method(): Y {}
16+
}
17+
var_dump(new B);
18+
} else if ($class == 'X') {
19+
class X {
20+
public function method(): Q {}
21+
}
22+
var_dump(new X);
23+
} else if ($class == 'Y') {
24+
class Y extends X {
25+
public function method(): R {}
26+
}
27+
var_dump(new Y);
28+
} else if ($class == 'Q') {
29+
class Q {
30+
public function method(): A {}
31+
}
32+
var_dump(new Q);
33+
} else if ($class == 'R') {
34+
class R extends Q {
35+
public function method(): B {}
36+
}
37+
var_dump(new R);
38+
}
39+
});
40+
41+
var_dump(new B);
42+
43+
?>
44+
===DONE===
45+
--EXPECT--
46+
object(A)#2 (0) {
47+
}
48+
object(X)#2 (0) {
49+
}
50+
object(Q)#2 (0) {
51+
}
52+
object(R)#2 (0) {
53+
}
54+
object(Y)#2 (0) {
55+
}
56+
object(B)#2 (0) {
57+
}
58+
object(B)#2 (0) {
59+
}
60+
===DONE===
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
--TEST--
2-
Returns are covariant, but we don't allow the code due to class ordering (autoload variation)
2+
Variance error in the presence of autoloading (1)
33
--FILE--
44
<?php
55

66
spl_autoload_register(function($class) {
77
if ($class === 'A') {
88
class A {
9-
public function method() : B {}
9+
public function method() : C {}
1010
}
1111
} else if ($class == 'B') {
1212
class B extends A {
13-
public function method() : C {}
13+
public function method() : B {}
1414
}
1515
} else {
1616
class C extends B {
1717
}
1818
}
1919
});
2020

21-
$c = new C;
21+
$b = new B;
2222

2323
?>
2424
--EXPECTF--
25-
Fatal error: Could not check compatibility between B::method(): C and A::method(): B, because class C is not available in %s on line %d
25+
Fatal error: Declaration of B::method(): B must be compatible with A::method(): C in %s on line %d
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
--TEST--
2+
Variance error in the presence of autoloading (2)
3+
--FILE--
4+
<?php
5+
6+
// Same as autoload_error1, but for argument types.
7+
spl_autoload_register(function($class) {
8+
if ($class === 'A') {
9+
class A {
10+
public function method(B $x) {}
11+
}
12+
} else if ($class == 'B') {
13+
class B extends A {
14+
public function method(C $x) {}
15+
}
16+
} else {
17+
class C extends B {
18+
}
19+
}
20+
});
21+
22+
$b = new B;
23+
$c = new C;
24+
25+
?>
26+
--EXPECTF--
27+
Warning: Declaration of B::method(C $x) should be compatible with A::method(B $x) in %s on line %d
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
Variance error in the presence of autoloading (3)
3+
--FILE--
4+
<?php
5+
6+
spl_autoload_register(function($class) {
7+
if ($class == 'A') {
8+
class A {
9+
public function method(): X {}
10+
}
11+
} else if ($class == 'B') {
12+
class B extends A {
13+
public function method(): Y {}
14+
}
15+
} else if ($class == 'X') {
16+
class X {
17+
public function method(): Q {}
18+
}
19+
} else if ($class == 'Y') {
20+
class Y extends X {
21+
public function method(): R {}
22+
}
23+
} else if ($class == 'Q') {
24+
class Q {
25+
public function method(): B {}
26+
}
27+
} else if ($class == 'R') {
28+
class R extends Q {
29+
public function method(): A {}
30+
}
31+
}
32+
});
33+
34+
$b = new B;
35+
36+
?>
37+
--EXPECTF--
38+
Fatal error: Declaration of R::method(): A must be compatible with Q::method(): B in %s on line %d
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
--TEST--
2+
Variance error in the presence of autoloading (4)
3+
--FILE--
4+
<?php
5+
6+
spl_autoload_register(function($class) {
7+
if ($class == 'A') {
8+
class A {
9+
public function method(): X {}
10+
}
11+
var_dump(new A);
12+
} else if ($class == 'B') {
13+
class B extends A {
14+
public function method(): Y {}
15+
}
16+
var_dump(new B);
17+
} else if ($class == 'X') {
18+
class X {
19+
public function method(): B {}
20+
}
21+
var_dump(new X);
22+
} else if ($class == 'Y') {
23+
class Y extends X {
24+
public function method(): A {}
25+
}
26+
var_dump(new Y);
27+
}
28+
});
29+
30+
var_dump(new B);
31+
32+
?>
33+
--EXPECTF--
34+
object(A)#2 (0) {
35+
}
36+
object(X)#2 (0) {
37+
}
38+
39+
Fatal error: Declaration of Y::method(): A must be compatible with X::method(): B in %s on line %d

0 commit comments

Comments
 (0)