Skip to content

Commit 5189eb6

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

File tree

4 files changed

+89
-14
lines changed

4 files changed

+89
-14
lines changed

src/functions.php

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -344,17 +344,10 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool
344344

345345
$expectedException = $parameters[0];
346346

347-
// PHP before v8 used an easy API:
348-
if (\PHP_VERSION_ID < 70100 || \defined('HHVM_VERSION')) {
349-
if (!$expectedException->getClass()) {
350-
return true;
351-
}
352-
353-
return $expectedException->getClass()->isInstance($reason);
354-
}
355-
356347
// Extract the type of the argument and handle different possibilities
357348
$type = $expectedException->getType();
349+
350+
$isTypeUnion = true;
358351
$types = [];
359352

360353
switch (true) {
@@ -363,6 +356,8 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool
363356
case $type instanceof \ReflectionNamedType:
364357
$types = [$type];
365358
break;
359+
case $type instanceof \ReflectionIntersectionType:
360+
$isTypeUnion = false;
366361
case $type instanceof \ReflectionUnionType;
367362
$types = $type->getTypes();
368363
break;
@@ -375,16 +370,30 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool
375370
return true;
376371
}
377372

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.
380373
foreach ($types as $type) {
374+
if (!$type instanceof \ReflectionNamedType) {
375+
throw new \LogicException('This implementation does not support groups of intersection or union types');
376+
}
377+
378+
// A named-type can be either a class-name or a built-in type like string, int, array, etc.
381379
$matches = ($type->isBuiltin() && \gettype($reason) === $type->getName())
382380
|| (new \ReflectionClass($type->getName()))->isInstance($reason);
383381

382+
383+
// If we look for a single match (union), we can return early on match
384+
// If we look for a full match (intersection), we can return early on mismatch
384385
if ($matches) {
385-
return true;
386+
if ($isTypeUnion) {
387+
return true;
388+
}
389+
} else {
390+
if (!$isTypeUnion) {
391+
return false;
392+
}
386393
}
387394
}
388395

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

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([CallbackWithIntersectionTypehintClass::class, 'testCallbackStatic'], new \RuntimeException()));
115+
self::assertTrue(_checkTypehint([CallbackWithIntersectionTypehintClass::class, '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+
}
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)