Skip to content

Commit 02ed10a

Browse files
committed
Extend _checkTypehint support for PHP8.1's intersection types
1 parent ae8eb1a commit 02ed10a

File tree

4 files changed

+89
-5
lines changed

4 files changed

+89
-5
lines changed

src/functions.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,8 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool
355355

356356
// Extract the type of the argument and handle different possibilities
357357
$type = $expectedException->getType();
358+
359+
$isTypeUnion = true;
358360
$types = [];
359361

360362
switch (true) {
@@ -363,6 +365,8 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool
363365
case $type instanceof \ReflectionNamedType:
364366
$types = [$type];
365367
break;
368+
case $type instanceof \ReflectionIntersectionType:
369+
$isTypeUnion = false;
366370
case $type instanceof \ReflectionUnionType;
367371
$types = $type->getTypes();
368372
break;
@@ -375,16 +379,30 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool
375379
return true;
376380
}
377381

378-
// Search for one matching named-type for success, otherwise return false
379-
// A named-type can be either a class-name or a built-in type like string, int, array, etc.
380382
foreach ($types as $type) {
383+
if (!$type instanceof \ReflectionNamedType) {
384+
throw new \LogicException('This implementation does not support groups of intersection or union types');
385+
}
386+
387+
// A named-type can be either a class-name or a built-in type like string, int, array, etc.
381388
$matches = ($type->isBuiltin() && \gettype($reason) === $type->getName())
382389
|| (new \ReflectionClass($type->getName()))->isInstance($reason);
383390

391+
392+
// If we look for a single match (union), we can return early on match
393+
// If we look for a full match (intersection), we can return early on mismatch
384394
if ($matches) {
385-
return true;
395+
if ($isTypeUnion) {
396+
return true;
397+
}
398+
} else {
399+
if (!$isTypeUnion) {
400+
return false;
401+
}
386402
}
387403
}
388404

389-
return false;
405+
// If we look for a single match (union) and did not return early, we matched no type and are false
406+
// If we look for a full match (intersection) and did not return early, we matched all types and are true
407+
return $isTypeUnion ? false : true;
390408
}

tests/FunctionCheckTypehintTest.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,37 @@ public function shouldAcceptStaticClassCallbackWithUnionTypehint()
8585
self::assertFalse(_checkTypehint([CallbackWithUnionTypehintClass::class, 'testCallbackStatic'], new Exception()));
8686
}
8787

88-
/** @test */
88+
/**
89+
* @test
90+
* @requires PHP 8.1
91+
*/
92+
public function shouldAcceptInvokableObjectCallbackWithIntersectionTypehint()
93+
{
94+
self::assertFalse(_checkTypehint(new CallbackWithIntersectionTypehintClass(), new \RuntimeException()));
95+
self::assertTrue(_checkTypehint(new CallbackWithIntersectionTypehintClass(), new CountableException()));
96+
}
97+
98+
/**
99+
* @test
100+
* @requires PHP 8.1
101+
*/
102+
public function shouldAcceptObjectMethodCallbackWithIntersectionTypehint()
103+
{
104+
self::assertFalse(_checkTypehint([new CallbackWithIntersectionTypehintClass(), 'testCallback'], new \RuntimeException()));
105+
self::assertTrue(_checkTypehint([new CallbackWithIntersectionTypehintClass(), 'testCallback'], new CountableException()));
106+
}
107+
108+
/**
109+
* @test
110+
* @requires PHP 8.1
111+
*/
112+
public function shouldAcceptStaticClassCallbackWithIntersectionTypehint()
113+
{
114+
self::assertFalse(_checkTypehint(['React\Promise\CallbackWithIntersectionTypehintClass', 'testCallbackStatic'], new \RuntimeException()));
115+
self::assertTrue(_checkTypehint(['React\Promise\CallbackWithIntersectionTypehintClass', 'testCallbackStatic'], new CountableException()));
116+
}
117+
118+
/** @test */
89119
public function shouldAcceptClosureCallbackWithoutTypehint()
90120
{
91121
self::assertTrue(_checkTypehint(function (InvalidArgumentException $e) {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace React\Promise;
4+
5+
use Countable;
6+
use RuntimeException;
7+
8+
class CallbackWithIntersectionTypehintClass
9+
{
10+
public function __invoke(RuntimeException&Countable $e)
11+
{
12+
}
13+
14+
public function testCallback(RuntimeException&Countable $e)
15+
{
16+
}
17+
18+
public static function testCallbackStatic(RuntimeException&Countable $e)
19+
{
20+
}
21+
}

tests/fixtures/CountableException.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace React\Promise;
4+
5+
use Countable;
6+
use RuntimeException;
7+
8+
class CountableException extends RuntimeException implements Countable
9+
{
10+
public function count(): int
11+
{
12+
return 0;
13+
}
14+
}
15+

0 commit comments

Comments
 (0)