Repository: reactphp/promise (php)

Issue:
Problem with PHP 8
The "otherwise" method on Promise seems to have a problem with PHP8
it will throw this exception : 

#message: "Method ReflectionParameter::getClass() is deprecated"
  #code: 0
  #file: "./vendor/react/promise/src/functions.php"
  #line: 346
  #severity: E_DEPRECATED

it seems the problem is caused by :
\React\Promise\Promise - Line : 77
\React\Promise - functions.php - Line : 346

https://github.com/reactphp/promise/blob/a9752a861e21c0fe0b380c9f9e55beddc0ed7d31/src/functions.php#L346


**update:**

This method is deprecated in the current version of PHP (8) :
[Reference](https://www.php.net/manual/en/reflectionparameter.getclass.php)

This notice can be turned off using native php **error_reporting()** function.
[Reference](https://www.php.net/manual/en/function.error-reporting.php)

run time error reporting setting :
error_reporting(error_reporting() & ~E_DEPRECATED);


Pull request:
Support intersection types (PHP 8.1+ / Promise v2)
PHP8's reflection API changed to make room for handling of union-types. As one of the consequences `ReflectionParameter::getClass` got marked as deprecated.
This commit moves the legacy path behind a version-check for PHP versions before 8 and adds a new more complex path for PHP8+.

Fixes #192.

Patch:
diff --git a/src/functions.php b/src/functions.php
index 68763db..429f0e7 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -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;
 }


Test patch:
diff --git a/tests/FunctionCheckTypehintTest.php b/tests/FunctionCheckTypehintTest.php
index 5c78ab3..b8d3fbe 100644
--- a/tests/FunctionCheckTypehintTest.php
+++ b/tests/FunctionCheckTypehintTest.php
@@ -84,6 +84,39 @@ class FunctionCheckTypehintTest extends TestCase
         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()
     {
diff --git a/tests/fixtures/CallbackWithIntersectionTypehintClass.php b/tests/fixtures/CallbackWithIntersectionTypehintClass.php
new file mode 100644
index 0000000..3e73347
--- /dev/null
+++ b/tests/fixtures/CallbackWithIntersectionTypehintClass.php
@@ -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)
+    {
+    }
+}
diff --git a/tests/fixtures/CountableException.php b/tests/fixtures/CountableException.php
new file mode 100644
index 0000000..5573d62
--- /dev/null
+++ b/tests/fixtures/CountableException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace React\Promise;
+
+use Countable;
+use RuntimeException;
+
+class CountableException extends RuntimeException implements Countable
+{
+    public function count()
+    {
+        return 0;
+    }
+}
+
diff --git a/tests/fixtures/CountableNonException.php b/tests/fixtures/CountableNonException.php
new file mode 100644
index 0000000..007e612
--- /dev/null
+++ b/tests/fixtures/CountableNonException.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace React\Promise;
+
+use Countable;
+use RuntimeException;
+
+class CountableNonException implements Countable
+{
+    public function count()
+    {
+        return 0;
+    }
+}
+