Skip to content

Commit

Permalink
Merge pull request #209 from bzikarsky/bz/union-types-v3
Browse files Browse the repository at this point in the history
Support intersection types (PHP 8.1+ / ported from v2 to v3)
  • Loading branch information
WyriHaximus authored Feb 5, 2022
2 parents 30284b0 + 5189eb6 commit 333ab4b
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 20 deletions.
63 changes: 44 additions & 19 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -342,33 +342,58 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool
return true;
}

$type = $parameters[0]->getType();

if (!$type) {
return true;
$expectedException = $parameters[0];

// Extract the type of the argument and handle different possibilities
$type = $expectedException->getType();

$isTypeUnion = true;
$types = [];

switch (true) {
case $type === null:
break;
case $type instanceof \ReflectionNamedType:
$types = [$type];
break;
case $type instanceof \ReflectionIntersectionType:
$isTypeUnion = false;
case $type instanceof \ReflectionUnionType;
$types = $type->getTypes();
break;
default:
throw new \LogicException('Unexpected return value of ReflectionParameter::getType');
}

$types = [$type];

if ($type instanceof \ReflectionUnionType) {
$types = $type->getTypes();
// If there is no type restriction, it matches
if (empty($types)) {
return true;
}

$mismatched = false;

foreach ($types as $type) {
if (!$type || $type->isBuiltin()) {
continue;
if (!$type instanceof \ReflectionNamedType) {
throw new \LogicException('This implementation does not support groups of intersection or union types');
}

$expectedClass = $type->getName();

if ($reason instanceof $expectedClass) {
return true;
// A named-type can be either a class-name or a built-in type like string, int, array, etc.
$matches = ($type->isBuiltin() && \gettype($reason) === $type->getName())
|| (new \ReflectionClass($type->getName()))->isInstance($reason);


// If we look for a single match (union), we can return early on match
// If we look for a full match (intersection), we can return early on mismatch
if ($matches) {
if ($isTypeUnion) {
return true;
}
} else {
if (!$isTypeUnion) {
return false;
}
}

$mismatched = true;
}

return !$mismatched;
// If we look for a single match (union) and did not return early, we matched no type and are false
// If we look for a full match (intersection) and did not return early, we matched all types and are true
return $isTypeUnion ? false : true;
}
32 changes: 31 additions & 1 deletion tests/FunctionCheckTypehintTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,37 @@ public function shouldAcceptStaticClassCallbackWithUnionTypehint()
self::assertFalse(_checkTypehint([CallbackWithUnionTypehintClass::class, 'testCallbackStatic'], new Exception()));
}

/** @test */
/**
* @test
* @requires PHP 8.1
*/
public function shouldAcceptInvokableObjectCallbackWithIntersectionTypehint()
{
self::assertFalse(_checkTypehint(new CallbackWithIntersectionTypehintClass(), new \RuntimeException()));
self::assertTrue(_checkTypehint(new CallbackWithIntersectionTypehintClass(), new CountableException()));
}

/**
* @test
* @requires PHP 8.1
*/
public function shouldAcceptObjectMethodCallbackWithIntersectionTypehint()
{
self::assertFalse(_checkTypehint([new CallbackWithIntersectionTypehintClass(), 'testCallback'], new \RuntimeException()));
self::assertTrue(_checkTypehint([new CallbackWithIntersectionTypehintClass(), 'testCallback'], new CountableException()));
}

/**
* @test
* @requires PHP 8.1
*/
public function shouldAcceptStaticClassCallbackWithIntersectionTypehint()
{
self::assertFalse(_checkTypehint([CallbackWithIntersectionTypehintClass::class, 'testCallbackStatic'], new \RuntimeException()));
self::assertTrue(_checkTypehint([CallbackWithIntersectionTypehintClass::class, 'testCallbackStatic'], new CountableException()));
}

/** @test */
public function shouldAcceptClosureCallbackWithoutTypehint()
{
self::assertTrue(_checkTypehint(function (InvalidArgumentException $e) {
Expand Down
21 changes: 21 additions & 0 deletions tests/fixtures/CallbackWithIntersectionTypehintClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace React\Promise;

use Countable;
use RuntimeException;

class CallbackWithIntersectionTypehintClass
{
public function __invoke(RuntimeException&Countable $e)
{
}

public function testCallback(RuntimeException&Countable $e)
{
}

public static function testCallbackStatic(RuntimeException&Countable $e)
{
}
}
15 changes: 15 additions & 0 deletions tests/fixtures/CountableException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace React\Promise;

use Countable;
use RuntimeException;

class CountableException extends RuntimeException implements Countable
{
public function count(): int
{
return 0;
}
}

0 comments on commit 333ab4b

Please sign in to comment.