Skip to content

Commit 333ab4b

Browse files
authored
Merge pull request #209 from bzikarsky/bz/union-types-v3
Support intersection types (PHP 8.1+ / ported from v2 to v3)
2 parents 30284b0 + 5189eb6 commit 333ab4b

File tree

4 files changed

+111
-20
lines changed

4 files changed

+111
-20
lines changed

src/functions.php

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -342,33 +342,58 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool
342342
return true;
343343
}
344344

345-
$type = $parameters[0]->getType();
346-
347-
if (!$type) {
348-
return true;
345+
$expectedException = $parameters[0];
346+
347+
// Extract the type of the argument and handle different possibilities
348+
$type = $expectedException->getType();
349+
350+
$isTypeUnion = true;
351+
$types = [];
352+
353+
switch (true) {
354+
case $type === null:
355+
break;
356+
case $type instanceof \ReflectionNamedType:
357+
$types = [$type];
358+
break;
359+
case $type instanceof \ReflectionIntersectionType:
360+
$isTypeUnion = false;
361+
case $type instanceof \ReflectionUnionType;
362+
$types = $type->getTypes();
363+
break;
364+
default:
365+
throw new \LogicException('Unexpected return value of ReflectionParameter::getType');
349366
}
350367

351-
$types = [$type];
352-
353-
if ($type instanceof \ReflectionUnionType) {
354-
$types = $type->getTypes();
368+
// If there is no type restriction, it matches
369+
if (empty($types)) {
370+
return true;
355371
}
356372

357-
$mismatched = false;
358-
359373
foreach ($types as $type) {
360-
if (!$type || $type->isBuiltin()) {
361-
continue;
374+
if (!$type instanceof \ReflectionNamedType) {
375+
throw new \LogicException('This implementation does not support groups of intersection or union types');
362376
}
363377

364-
$expectedClass = $type->getName();
365-
366-
if ($reason instanceof $expectedClass) {
367-
return true;
378+
// A named-type can be either a class-name or a built-in type like string, int, array, etc.
379+
$matches = ($type->isBuiltin() && \gettype($reason) === $type->getName())
380+
|| (new \ReflectionClass($type->getName()))->isInstance($reason);
381+
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
385+
if ($matches) {
386+
if ($isTypeUnion) {
387+
return true;
388+
}
389+
} else {
390+
if (!$isTypeUnion) {
391+
return false;
392+
}
368393
}
369-
370-
$mismatched = true;
371394
}
372395

373-
return !$mismatched;
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;
374399
}

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+
}

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)