Skip to content

Commit 523ec08

Browse files
committed
Cover forward_static_call() across a multi-level hierarchy
Root -> Middle -> Leaf (final), each with its own which() implementation carrying a distinct return type, mirroring the runtime behavior: - naming an ancestor calls the named class's implementation (overrides in the caller or below it do not re-dispatch) while the binding is forwarded, - the forwarded binding is the caller's static, collapsing to the class itself in a final class, - naming a descendant resolves to the named class's object type, which covers both runtime outcomes (binding forwarded when the runtime static is a subclass of the named class, reset to the named class otherwise), - an instance method has an active class scope to forward as well.
1 parent 376b377 commit 523ec08

1 file changed

Lines changed: 98 additions & 0 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ForwardStaticCallMultiLevel;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* Multi-level hierarchy: Root -> Middle -> Leaf (final).
9+
* Each class has its own which() implementation with a distinct return type, so the
10+
* assertions below can tell exactly which implementation the call is resolved against.
11+
*/
12+
class Root
13+
{
14+
15+
/** @return 'Root' */
16+
public static function which(): string
17+
{
18+
return 'Root';
19+
}
20+
21+
/** @return static */
22+
public static function create(): static
23+
{
24+
return new static(); // @phpstan-ignore new.static
25+
}
26+
27+
}
28+
29+
class Middle extends Root
30+
{
31+
32+
/** @return 'Middle' */
33+
public static function which(): string // @phpstan-ignore method.childReturnType
34+
{
35+
return 'Middle';
36+
}
37+
38+
/** @return static */
39+
public static function make(): static
40+
{
41+
return new static(); // @phpstan-ignore new.static
42+
}
43+
44+
public static function test(): void
45+
{
46+
// Naming an ancestor calls the *named* class's implementation (runtime returns
47+
// 'Root' even though Middle and Leaf override which()), while the late static
48+
// binding is forwarded.
49+
assertType("'Root'", forward_static_call([Root::class, 'which']));
50+
assertType("'Root'", forward_static_call('ForwardStaticCallMultiLevel\Root::which'));
51+
52+
// The forwarded binding is the caller's static: at runtime Middle::test() gives
53+
// Middle and Leaf::test() gives Leaf, both covered by static(Middle).
54+
assertType('static(ForwardStaticCallMultiLevel\Middle)', forward_static_call([Root::class, 'create']));
55+
assertType('static(ForwardStaticCallMultiLevel\Middle)', forward_static_call('ForwardStaticCallMultiLevel\Root::create'));
56+
57+
// self::class names this very class; its own implementation is called.
58+
assertType("'Middle'", forward_static_call([self::class, 'which']));
59+
assertType('static(ForwardStaticCallMultiLevel\Middle)', forward_static_call([self::class, 'make']));
60+
61+
// Naming a descendant: at runtime the binding is forwarded only when the caller's
62+
// runtime static is a subclass of the named class (Leaf::test() gives Leaf,
63+
// Middle::test() gives Leaf too because the binding resets to the named class),
64+
// so the named class's object type covers both outcomes.
65+
assertType('ForwardStaticCallMultiLevel\Leaf', forward_static_call([Leaf::class, 'create']));
66+
}
67+
68+
public function instanceContext(): void
69+
{
70+
// An instance method also has an active class scope to forward
71+
// (at runtime the binding is the object's class).
72+
assertType('static(ForwardStaticCallMultiLevel\Middle)', forward_static_call([Root::class, 'create']));
73+
}
74+
75+
}
76+
77+
final class Leaf extends Middle
78+
{
79+
80+
/** @return 'Leaf' */
81+
public static function which(): string // @phpstan-ignore method.childReturnType
82+
{
83+
return 'Leaf';
84+
}
85+
86+
public static function test(): void
87+
{
88+
// Called from the final class, naming the two-levels-up ancestor: still the named
89+
// class's implementation (runtime returns 'Root', not this class's override), and
90+
// the forwarded binding collapses to the final class itself.
91+
assertType("'Root'", forward_static_call([Root::class, 'which']));
92+
assertType('ForwardStaticCallMultiLevel\Leaf', forward_static_call([Root::class, 'create']));
93+
94+
// A method defined only in the intermediate class forwards the same way.
95+
assertType('ForwardStaticCallMultiLevel\Leaf', forward_static_call([Middle::class, 'make']));
96+
}
97+
98+
}

0 commit comments

Comments
 (0)