Repository: reactphp/promise (php)

Issue:
Support Disjunctive Normal Form Types (DNF types) for PHP 8.2+
We should support Disjunctive Normal Form Types (DNF types) for PHP 8.2+ (RFC: https://wiki.php.net/rfc/dnf_types):

```php
$promise->otherwise(function ((A&B)|(C&D) $e) { }); // Promise v2
$promise->catch(function ((A&B)|(C&D) $e) { }); // Promise v3
```

* Support for union types (PHP 8+) has been added via #198 (Promise v2) and #197 (Promise v3)
* Support for intersection types (PHP 8.1+) has been added via #195 (Promise v2) and #209 (Promise v3)
* The `catch()` method exists for Promise v3 only, the `otherwise()` method has been deprecated for Promise v3 via #208
* There's currently no support for DNF types in either Promise v2 or Promise v3. Support should be added by adding this feature to either version (preferable Promise v3) and then cherry-picking to the other version.
* Legacy Promise v1 is not affected as it does not have an `otherwise()` or `catch()` method that checks exception types.
* DNF types will be supported in PHP 8.2+ only which is scheduled to be released in November.
* Other than that, this project supports PHP 8.2+ just fine, see #230 and #234

Input and PRs very much welcome! :+1: 

Pull request description:
[3.x] feat: add support for DNF types
This PR adds support for Disjunctive Normal Form Types (DNF types) when using PHP 8.2.

### Overview of the changes

- [x] Removes the previous guard that would only allow `ReflectionNamedType` to be analyzed.
- [x] ~Adds two new **internal** functions `matchesType` and `matchesAllTypes` to extract common functionality.~
- [x] Adds tests + supporting fixtures

Please let me know if you think that I missed something.

---

Closes #235 

Patch:
diff --git a/src/functions.php b/src/functions.php
index fbe4961..7517d6a 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -267,14 +267,18 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool
     }
 
     foreach ($types as $type) {
-        if (!$type instanceof \ReflectionNamedType) {
-            throw new \LogicException('This implementation does not support groups of intersection or union types');
-        }
-
-        // 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 ($type instanceof \ReflectionIntersectionType) {
+            foreach ($type->getTypes() as $typeToMatch) {
+                if (!($matches = ($typeToMatch->isBuiltin() && \gettype($reason) === $typeToMatch->getName())
+                    || (new \ReflectionClass($typeToMatch->getName()))->isInstance($reason))) {
+                    break;
+                }
+            }
+        } else {
+            $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


Test patch:
diff --git a/tests/FunctionCheckTypehintTest.php b/tests/FunctionCheckTypehintTest.php
index b263473..eb3b4a3 100644
--- a/tests/FunctionCheckTypehintTest.php
+++ b/tests/FunctionCheckTypehintTest.php
@@ -115,6 +115,39 @@ class FunctionCheckTypehintTest extends TestCase
         self::assertTrue(_checkTypehint([CallbackWithIntersectionTypehintClass::class, 'testCallbackStatic'], new CountableException()));
     }
 
+    /**
+     * @test
+     * @requires PHP 8.2
+     */
+    public function shouldAcceptInvokableObjectCallbackWithDNFTypehint()
+    {
+        self::assertFalse(_checkTypehint(new CallbackWithDNFTypehintClass(), new \RuntimeException()));
+        self::assertTrue(_checkTypehint(new CallbackWithDNFTypehintClass(), new ArrayAccessibleException()));
+        self::assertTrue(_checkTypehint(new CallbackWithDNFTypehintClass(), new CountableException()));
+    }
+
+    /**
+     * @test
+     * @requires PHP 8.2
+     */
+    public function shouldAcceptObjectMethodCallbackWithDNFTypehint()
+    {
+        self::assertFalse(_checkTypehint([new CallbackWithDNFTypehintClass(), 'testCallback'], new \RuntimeException()));
+        self::assertTrue(_checkTypehint([new CallbackWithDNFTypehintClass(), 'testCallback'], new CountableException()));
+        self::assertTrue(_checkTypehint([new CallbackWithDNFTypehintClass(), 'testCallback'], new ArrayAccessibleException()));
+    }
+
+    /**
+     * @test
+     * @requires PHP 8.2
+     */
+    public function shouldAcceptStaticClassCallbackWithDNFTypehint()
+    {
+        self::assertFalse(_checkTypehint([CallbackWithDNFTypehintClass::class, 'testCallbackStatic'], new \RuntimeException()));
+        self::assertTrue(_checkTypehint([CallbackWithDNFTypehintClass::class, 'testCallbackStatic'], new CountableException()));
+        self::assertTrue(_checkTypehint([CallbackWithDNFTypehintClass::class, 'testCallbackStatic'], new ArrayAccessibleException()));
+    }
+
     /** @test */
     public function shouldAcceptClosureCallbackWithoutTypehint()
     {
diff --git a/tests/fixtures/ArrayAccessibleException.php b/tests/fixtures/ArrayAccessibleException.php
new file mode 100644
index 0000000..71b7ad2
--- /dev/null
+++ b/tests/fixtures/ArrayAccessibleException.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace React\Promise;
+
+use RuntimeException;
+
+class ArrayAccessibleException extends RuntimeException implements \ArrayAccess
+{
+    public function offsetExists(mixed $offset): bool
+    {
+        return true;
+    }
+
+    public function offsetGet(mixed $offset): mixed
+    {
+        return $offset;
+    }
+
+    public function offsetSet(mixed $offset, mixed $value): void {}
+
+    public function offsetUnset(mixed $offset): void {}
+}
diff --git a/tests/fixtures/CallbackWithDNFTypehintClass.php b/tests/fixtures/CallbackWithDNFTypehintClass.php
new file mode 100644
index 0000000..c48ff2e
--- /dev/null
+++ b/tests/fixtures/CallbackWithDNFTypehintClass.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace React\Promise;
+
+use Countable;
+use RuntimeException;
+
+class CallbackWithDNFTypehintClass
+{
+    public function __invoke((RuntimeException&Countable)|(RuntimeException&\ArrayAccess) $e)
+    {
+    }
+
+    public function testCallback((RuntimeException&Countable)|(RuntimeException&\ArrayAccess) $e)
+    {
+    }
+
+    public static function testCallbackStatic((RuntimeException&Countable)|(RuntimeException&\ArrayAccess) $e)
+    {
+    }
+}