Extend _checkTypehint support for PHP8.1's intersection types
Benjamin Zikarsky
2 years ago
343 | 343 | |
344 | 344 | $expectedException = $parameters[0]; |
345 | 345 | |
346 | // PHP before v8 used an easy API: | |
347 | if (\PHP_VERSION_ID < 70100 || \defined('HHVM_VERSION')) { | |
348 | if (!$expectedException->getClass()) { | |
349 | return true; | |
350 | } | |
351 | ||
352 | return $expectedException->getClass()->isInstance($reason); | |
353 | } | |
354 | ||
355 | 346 | // Extract the type of the argument and handle different possibilities |
356 | 347 | $type = $expectedException->getType(); |
348 | ||
349 | $isTypeUnion = true; | |
357 | 350 | $types = []; |
358 | 351 | |
359 | 352 | switch (true) { |
362 | 355 | case $type instanceof \ReflectionNamedType: |
363 | 356 | $types = [$type]; |
364 | 357 | break; |
358 | case $type instanceof \ReflectionIntersectionType: | |
359 | $isTypeUnion = false; | |
365 | 360 | case $type instanceof \ReflectionUnionType; |
366 | 361 | $types = $type->getTypes(); |
367 | 362 | break; |
374 | 369 | return true; |
375 | 370 | } |
376 | 371 | |
377 | // Search for one matching named-type for success, otherwise return false | |
378 | // A named-type can be either a class-name or a built-in type like string, int, array, etc. | |
379 | 372 | foreach ($types as $type) { |
373 | if (!$type instanceof \ReflectionNamedType) { | |
374 | throw new \LogicException('This implementation does not support groups of intersection or union types'); | |
375 | } | |
376 | ||
377 | // A named-type can be either a class-name or a built-in type like string, int, array, etc. | |
380 | 378 | $matches = ($type->isBuiltin() && \gettype($reason) === $type->getName()) |
381 | 379 | || (new \ReflectionClass($type->getName()))->isInstance($reason); |
382 | 380 | |
381 | ||
382 | // If we look for a single match (union), we can return early on match | |
383 | // If we look for a full match (intersection), we can return early on mismatch | |
383 | 384 | if ($matches) { |
384 | return true; | |
385 | } | |
386 | } | |
387 | ||
388 | return false; | |
389 | } | |
385 | if ($isTypeUnion) { | |
386 | return true; | |
387 | } | |
388 | } else { | |
389 | if (!$isTypeUnion) { | |
390 | return false; | |
391 | } | |
392 | } | |
393 | } | |
394 | ||
395 | // If we look for a single match (union) and did not return early, we matched no type and are false | |
396 | // If we look for a full match (intersection) and did not return early, we matched all types and are true | |
397 | return $isTypeUnion ? false : true; | |
398 | } |
84 | 84 | self::assertFalse(_checkTypehint([CallbackWithUnionTypehintClass::class, 'testCallbackStatic'], new Exception())); |
85 | 85 | } |
86 | 86 | |
87 | /** @test */ | |
87 | /** | |
88 | * @test | |
89 | * @requires PHP 8.1 | |
90 | */ | |
91 | public function shouldAcceptInvokableObjectCallbackWithIntersectionTypehint() | |
92 | { | |
93 | self::assertFalse(_checkTypehint(new CallbackWithIntersectionTypehintClass(), new \RuntimeException())); | |
94 | self::assertTrue(_checkTypehint(new CallbackWithIntersectionTypehintClass(), new CountableException())); | |
95 | } | |
96 | ||
97 | /** | |
98 | * @test | |
99 | * @requires PHP 8.1 | |
100 | */ | |
101 | public function shouldAcceptObjectMethodCallbackWithIntersectionTypehint() | |
102 | { | |
103 | self::assertFalse(_checkTypehint([new CallbackWithIntersectionTypehintClass(), 'testCallback'], new \RuntimeException())); | |
104 | self::assertTrue(_checkTypehint([new CallbackWithIntersectionTypehintClass(), 'testCallback'], new CountableException())); | |
105 | } | |
106 | ||
107 | /** | |
108 | * @test | |
109 | * @requires PHP 8.1 | |
110 | */ | |
111 | public function shouldAcceptStaticClassCallbackWithIntersectionTypehint() | |
112 | { | |
113 | self::assertFalse(_checkTypehint([CallbackWithIntersectionTypehintClass::class, 'testCallbackStatic'], new \RuntimeException())); | |
114 | self::assertTrue(_checkTypehint([CallbackWithIntersectionTypehintClass::class, 'testCallbackStatic'], new CountableException())); | |
115 | } | |
116 | ||
117 | /** @test */ | |
88 | 118 | public function shouldAcceptClosureCallbackWithoutTypehint() |
89 | 119 | { |
90 | 120 | self::assertTrue(_checkTypehint(function (InvalidArgumentException $e) { |
0 | <?php | |
1 | ||
2 | namespace React\Promise; | |
3 | ||
4 | use Countable; | |
5 | use RuntimeException; | |
6 | ||
7 | class CallbackWithIntersectionTypehintClass | |
8 | { | |
9 | public function __invoke(RuntimeException&Countable $e) | |
10 | { | |
11 | } | |
12 | ||
13 | public function testCallback(RuntimeException&Countable $e) | |
14 | { | |
15 | } | |
16 | ||
17 | public static function testCallbackStatic(RuntimeException&Countable $e) | |
18 | { | |
19 | } | |
20 | } |