diff --git a/src/functions.php b/src/functions.php index 31e7db7b..df50eb33 100644 --- a/src/functions.php +++ b/src/functions.php @@ -342,11 +342,33 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool return true; } - $expectedClass = $parameters[0]->getClass(); + $type = $parameters[0]->getType(); - if (!$expectedClass) { + if (!$type) { return true; } - return $expectedClass->isInstance($reason); + $types = [$type]; + + if ($type instanceof \ReflectionUnionType) { + $types = $type->getTypes(); + } + + $mismatched = false; + + foreach ($types as $type) { + if (!$type || $type->isBuiltin()) { + continue; + } + + $expectedClass = $type->getName(); + + if ($reason instanceof $expectedClass) { + return true; + } + + $mismatched = true; + } + + return !$mismatched; } diff --git a/tests/ErrorCollector.php b/tests/ErrorCollector.php index 970f8628..0ad3fffd 100644 --- a/tests/ErrorCollector.php +++ b/tests/ErrorCollector.php @@ -10,8 +10,8 @@ public function start() { $errors = []; - set_error_handler(function ($errno, $errstr, $errfile, $errline, $errcontext) use (&$errors) { - $errors[] = compact('errno', 'errstr', 'errfile', 'errline', 'errcontext'); + set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$errors) { + $errors[] = compact('errno', 'errstr', 'errfile', 'errline'); }); $this->errors = &$errors; diff --git a/tests/FunctionCheckTypehintTest.php b/tests/FunctionCheckTypehintTest.php index bebda2c7..37a438d7 100644 --- a/tests/FunctionCheckTypehintTest.php +++ b/tests/FunctionCheckTypehintTest.php @@ -17,29 +17,72 @@ public function shouldAcceptClosureCallbackWithTypehint() /** @test */ public function shouldAcceptFunctionStringCallbackWithTypehint() { - self::assertTrue(_checkTypehint(new TestCallbackWithTypehintClass(), new InvalidArgumentException())); - self::assertFalse(_checkTypehint(new TestCallbackWithTypehintClass(), new Exception())); + self::assertTrue(_checkTypehint(new CallbackWithTypehintClass(), new InvalidArgumentException())); + self::assertFalse(_checkTypehint(new CallbackWithTypehintClass(), new Exception())); } /** @test */ public function shouldAcceptInvokableObjectCallbackWithTypehint() { - self::assertTrue(_checkTypehint(new TestCallbackWithTypehintClass(), new InvalidArgumentException())); - self::assertFalse(_checkTypehint(new TestCallbackWithTypehintClass(), new Exception())); + self::assertTrue(_checkTypehint(new CallbackWithTypehintClass(), new InvalidArgumentException())); + self::assertFalse(_checkTypehint(new CallbackWithTypehintClass(), new Exception())); } /** @test */ public function shouldAcceptObjectMethodCallbackWithTypehint() { - self::assertTrue(_checkTypehint([new TestCallbackWithTypehintClass(), 'testCallback'], new InvalidArgumentException())); - self::assertFalse(_checkTypehint([new TestCallbackWithTypehintClass(), 'testCallback'], new Exception())); + self::assertTrue(_checkTypehint([new CallbackWithTypehintClass(), 'testCallback'], new InvalidArgumentException())); + self::assertFalse(_checkTypehint([new CallbackWithTypehintClass(), 'testCallback'], new Exception())); } /** @test */ public function shouldAcceptStaticClassCallbackWithTypehint() { - self::assertTrue(_checkTypehint([TestCallbackWithTypehintClass::class, 'testCallbackStatic'], new InvalidArgumentException())); - self::assertFalse(_checkTypehint([TestCallbackWithTypehintClass::class, 'testCallbackStatic'], new Exception())); + self::assertTrue(_checkTypehint([CallbackWithTypehintClass::class, 'testCallbackStatic'], new InvalidArgumentException())); + self::assertFalse(_checkTypehint([CallbackWithTypehintClass::class, 'testCallbackStatic'], new Exception())); + } + + /** + * @test + * @requires PHP 8 + */ + public function shouldAcceptClosureCallbackWithUnionTypehint() + { + eval( + 'namespace React\Promise;' . + 'self::assertTrue(_checkTypehint(function (\RuntimeException|\InvalidArgumentException $e) {}, new \InvalidArgumentException()));' . + 'self::assertFalse(_checkTypehint(function (\RuntimeException|\InvalidArgumentException $e) {}, new \Exception()));' + ); + } + + /** + * @test + * @requires PHP 8 + */ + public function shouldAcceptInvokableObjectCallbackWithUnionTypehint() + { + self::assertTrue(_checkTypehint(new CallbackWithUnionTypehintClass(), new InvalidArgumentException())); + self::assertFalse(_checkTypehint(new CallbackWithUnionTypehintClass(), new Exception())); + } + + /** + * @test + * @requires PHP 8 + */ + public function shouldAcceptObjectMethodCallbackWithUnionTypehint() + { + self::assertTrue(_checkTypehint([new CallbackWithUnionTypehintClass(), 'testCallback'], new InvalidArgumentException())); + self::assertFalse(_checkTypehint([new CallbackWithUnionTypehintClass(), 'testCallback'], new Exception())); + } + + /** + * @test + * @requires PHP 8 + */ + public function shouldAcceptStaticClassCallbackWithUnionTypehint() + { + self::assertTrue(_checkTypehint([CallbackWithUnionTypehintClass::class, 'testCallbackStatic'], new InvalidArgumentException())); + self::assertFalse(_checkTypehint([CallbackWithUnionTypehintClass::class, 'testCallbackStatic'], new Exception())); } /** @test */ @@ -52,25 +95,25 @@ public function shouldAcceptClosureCallbackWithoutTypehint() /** @test */ public function shouldAcceptFunctionStringCallbackWithoutTypehint() { - self::assertTrue(_checkTypehint(new TestCallbackWithTypehintClass(), new InvalidArgumentException())); + self::assertTrue(_checkTypehint(new CallbackWithoutTypehintClass(), new InvalidArgumentException())); } /** @test */ public function shouldAcceptInvokableObjectCallbackWithoutTypehint() { - self::assertTrue(_checkTypehint(new TestCallbackWithoutTypehintClass(), new InvalidArgumentException())); + self::assertTrue(_checkTypehint(new CallbackWithoutTypehintClass(), new InvalidArgumentException())); } /** @test */ public function shouldAcceptObjectMethodCallbackWithoutTypehint() { - self::assertTrue(_checkTypehint([new TestCallbackWithoutTypehintClass(), 'testCallback'], new InvalidArgumentException())); + self::assertTrue(_checkTypehint([new CallbackWithoutTypehintClass(), 'testCallback'], new InvalidArgumentException())); } /** @test */ public function shouldAcceptStaticClassCallbackWithoutTypehint() { - self::assertTrue(_checkTypehint([TestCallbackWithoutTypehintClass::class, 'testCallbackStatic'], new InvalidArgumentException())); + self::assertTrue(_checkTypehint([CallbackWithoutTypehintClass::class, 'testCallbackStatic'], new InvalidArgumentException())); } } @@ -81,33 +124,3 @@ function testCallbackWithTypehint(InvalidArgumentException $e) function testCallbackWithoutTypehint() { } - -class TestCallbackWithTypehintClass -{ - public function __invoke(InvalidArgumentException $e) - { - } - - public function testCallback(InvalidArgumentException $e) - { - } - - public static function testCallbackStatic(InvalidArgumentException $e) - { - } -} - -class TestCallbackWithoutTypehintClass -{ - public function __invoke() - { - } - - public function testCallback() - { - } - - public static function testCallbackStatic() - { - } -} diff --git a/tests/fixtures/CallbackWithTypehintClass.php b/tests/fixtures/CallbackWithTypehintClass.php new file mode 100644 index 00000000..12dd0da4 --- /dev/null +++ b/tests/fixtures/CallbackWithTypehintClass.php @@ -0,0 +1,20 @@ +