Skip to content

Commit 0e890c8

Browse files
authored
Merge pull request #195 from bzikarsky/patch-1
Support intersection types (PHP 8.1+ / Promise v2)
2 parents 29daf46 + 3580280 commit 0e890c8

File tree

5 files changed

+130
-22
lines changed

5 files changed

+130
-22
lines changed

src/functions.php

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -341,43 +341,67 @@ function _checkTypehint(callable $callback, $object)
341341
return true;
342342
}
343343

344-
if (\PHP_VERSION_ID < 70100 || \defined('HHVM_VERSION')) {
345-
$expectedException = $parameters[0];
344+
$expectedException = $parameters[0];
346345

346+
// PHP before v8 used an easy API:
347+
if (\PHP_VERSION_ID < 70100 || \defined('HHVM_VERSION')) {
347348
if (!$expectedException->getClass()) {
348349
return true;
349350
}
350351

351352
return $expectedException->getClass()->isInstance($object);
352-
} else {
353-
$type = $parameters[0]->getType();
353+
}
354354

355-
if (!$type) {
356-
return true;
357-
}
355+
// Extract the type of the argument and handle different possibilities
356+
$type = $expectedException->getType();
357+
358+
$isTypeUnion = true;
359+
$types = [];
360+
361+
switch (true) {
362+
case $type === null:
363+
break;
364+
case $type instanceof \ReflectionNamedType:
365+
$types = [$type];
366+
break;
367+
case $type instanceof \ReflectionIntersectionType:
368+
$isTypeUnion = false;
369+
case $type instanceof \ReflectionUnionType;
370+
$types = $type->getTypes();
371+
break;
372+
default:
373+
throw new \LogicException('Unexpected return value of ReflectionParameter::getType');
374+
}
358375

359-
$types = [$type];
376+
// If there is no type restriction, it matches
377+
if (empty($types)) {
378+
return true;
379+
}
360380

361-
if ($type instanceof \ReflectionUnionType) {
362-
$types = $type->getTypes();
381+
foreach ($types as $type) {
382+
if (!$type instanceof \ReflectionNamedType) {
383+
throw new \LogicException('This implementation does not support groups of intersection or union types');
363384
}
364385

365-
$mismatched = false;
366-
367-
foreach ($types as $type) {
368-
if (!$type || $type->isBuiltin()) {
369-
continue;
370-
}
386+
// A named-type can be either a class-name or a built-in type like string, int, array, etc.
387+
$matches = ($type->isBuiltin() && \gettype($object) === $type->getName())
388+
|| (new \ReflectionClass($type->getName()))->isInstance($object);
371389

372-
$expectedClass = $type->getName();
373390

374-
if ($object instanceof $expectedClass) {
391+
// If we look for a single match (union), we can return early on match
392+
// If we look for a full match (intersection), we can return early on mismatch
393+
if ($matches) {
394+
if ($isTypeUnion) {
375395
return true;
376396
}
377-
378-
$mismatched = true;
397+
} else {
398+
if (!$isTypeUnion) {
399+
return false;
400+
}
379401
}
380-
381-
return !$mismatched;
382402
}
403+
404+
// If we look for a single match (union) and did not return early, we matched no type and are false
405+
// If we look for a full match (intersection) and did not return early, we matched all types and are true
406+
return $isTypeUnion ? false : true;
383407
}

tests/FunctionCheckTypehintTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,39 @@ public function shouldAcceptStaticClassCallbackWithUnionTypehint()
8484
self::assertFalse(_checkTypehint(['React\Promise\CallbackWithUnionTypehintClass', 'testCallbackStatic'], new \Exception()));
8585
}
8686

87+
/**
88+
* @test
89+
* @requires PHP 8.1
90+
*/
91+
public function shouldAcceptInvokableObjectCallbackWithIntersectionTypehint()
92+
{
93+
self::assertFalse(_checkTypehint(new CallbackWithIntersectionTypehintClass(), new \RuntimeException()));
94+
self::assertFalse(_checkTypehint(new CallbackWithIntersectionTypehintClass(), new CountableNonException()));
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::assertFalse(_checkTypehint([new CallbackWithIntersectionTypehintClass(), 'testCallback'], new CountableNonException()));
106+
self::assertTrue(_checkTypehint([new CallbackWithIntersectionTypehintClass(), 'testCallback'], new CountableException()));
107+
}
108+
109+
/**
110+
* @test
111+
* @requires PHP 8.1
112+
*/
113+
public function shouldAcceptStaticClassCallbackWithIntersectionTypehint()
114+
{
115+
self::assertFalse(_checkTypehint(['React\Promise\CallbackWithIntersectionTypehintClass', 'testCallbackStatic'], new \RuntimeException()));
116+
self::assertFalse(_checkTypehint(['React\Promise\CallbackWithIntersectionTypehintClass', 'testCallbackStatic'], new CountableNonException()));
117+
self::assertTrue(_checkTypehint(['React\Promise\CallbackWithIntersectionTypehintClass', 'testCallbackStatic'], new CountableException()));
118+
}
119+
87120
/** @test */
88121
public function shouldAcceptClosureCallbackWithoutTypehint()
89122
{
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()
11+
{
12+
return 0;
13+
}
14+
}
15+
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 CountableNonException implements Countable
9+
{
10+
public function count()
11+
{
12+
return 0;
13+
}
14+
}
15+

0 commit comments

Comments
 (0)