Skip to content

Commit

Permalink
Merge pull request #195 from bzikarsky/patch-1
Browse files Browse the repository at this point in the history
Support intersection types (PHP 8.1+ / Promise v2)
  • Loading branch information
clue authored Feb 4, 2022
2 parents 29daf46 + 3580280 commit 0e890c8
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 22 deletions.
68 changes: 46 additions & 22 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -341,43 +341,67 @@ function _checkTypehint(callable $callback, $object)
return true;
}

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

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

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

if (!$type) {
return true;
}
// 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 there is no type restriction, it matches
if (empty($types)) {
return true;
}

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

$mismatched = false;

foreach ($types as $type) {
if (!$type || $type->isBuiltin()) {
continue;
}
// A named-type can be either a class-name or a built-in type like string, int, array, etc.
$matches = ($type->isBuiltin() && \gettype($object) === $type->getName())
|| (new \ReflectionClass($type->getName()))->isInstance($object);

$expectedClass = $type->getName();

if ($object instanceof $expectedClass) {
// 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;
}

$mismatched = true;
} else {
if (!$isTypeUnion) {
return false;
}
}

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;
}
33 changes: 33 additions & 0 deletions tests/FunctionCheckTypehintTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,39 @@ public function shouldAcceptStaticClassCallbackWithUnionTypehint()
self::assertFalse(_checkTypehint(['React\Promise\CallbackWithUnionTypehintClass', 'testCallbackStatic'], new \Exception()));
}

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

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

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

/** @test */
public function shouldAcceptClosureCallbackWithoutTypehint()
{
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()
{
return 0;
}
}

15 changes: 15 additions & 0 deletions tests/fixtures/CountableNonException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace React\Promise;

use Countable;
use RuntimeException;

class CountableNonException implements Countable
{
public function count()
{
return 0;
}
}

0 comments on commit 0e890c8

Please sign in to comment.