From ae8eb1a69fd135e0d8acb80f22f33ab1ccf28964 Mon Sep 17 00:00:00 2001 From: Benjamin Zikarsky <225374+bzikarsky@users.noreply.github.com> Date: Tue, 28 Sep 2021 11:34:49 +0200 Subject: [PATCH 1/2] Port _checkType implementation from v2 --- src/functions.php | 50 +++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/src/functions.php b/src/functions.php index df50eb33..f621b9a9 100644 --- a/src/functions.php +++ b/src/functions.php @@ -342,33 +342,49 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool return true; } - $type = $parameters[0]->getType(); + $expectedException = $parameters[0]; - if (!$type) { - return true; - } + // PHP before v8 used an easy API: + if (\PHP_VERSION_ID < 70100 || \defined('HHVM_VERSION')) { + if (!$expectedException->getClass()) { + return true; + } - $types = [$type]; + return $expectedException->getClass()->isInstance($reason); + } - if ($type instanceof \ReflectionUnionType) { - $types = $type->getTypes(); + // Extract the type of the argument and handle different possibilities + $type = $expectedException->getType(); + $types = []; + + switch (true) { + case $type === null: + break; + case $type instanceof \ReflectionNamedType: + $types = [$type]; + break; + case $type instanceof \ReflectionUnionType; + $types = $type->getTypes(); + break; + default: + throw new \LogicException('Unexpected return value of ReflectionParameter::getType'); } - $mismatched = false; + // If there is no type restriction, it matches + if (empty($types)) { + return true; + } + // Search for one matching named-type for success, otherwise return false + // A named-type can be either a class-name or a built-in type like string, int, array, etc. foreach ($types as $type) { - if (!$type || $type->isBuiltin()) { - continue; - } + $matches = ($type->isBuiltin() && \gettype($reason) === $type->getName()) + || (new \ReflectionClass($type->getName()))->isInstance($reason); - $expectedClass = $type->getName(); - - if ($reason instanceof $expectedClass) { + if ($matches) { return true; } - - $mismatched = true; } - return !$mismatched; + return false; } From 5189eb6f3b372f4c47dd2c94c4c454c3dedfbdfd Mon Sep 17 00:00:00 2001 From: Benjamin Zikarsky Date: Wed, 29 Sep 2021 09:13:32 +0200 Subject: [PATCH 2/2] Extend _checkTypehint support for PHP8.1's intersection types --- src/functions.php | 35 ++++++++++++------- tests/FunctionCheckTypehintTest.php | 32 ++++++++++++++++- .../CallbackWithIntersectionTypehintClass.php | 21 +++++++++++ tests/fixtures/CountableException.php | 15 ++++++++ 4 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 tests/fixtures/CallbackWithIntersectionTypehintClass.php create mode 100644 tests/fixtures/CountableException.php diff --git a/src/functions.php b/src/functions.php index f621b9a9..96106016 100644 --- a/src/functions.php +++ b/src/functions.php @@ -344,17 +344,10 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool $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($reason); - } - // Extract the type of the argument and handle different possibilities $type = $expectedException->getType(); + + $isTypeUnion = true; $types = []; switch (true) { @@ -363,6 +356,8 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool case $type instanceof \ReflectionNamedType: $types = [$type]; break; + case $type instanceof \ReflectionIntersectionType: + $isTypeUnion = false; case $type instanceof \ReflectionUnionType; $types = $type->getTypes(); break; @@ -375,16 +370,30 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool return true; } - // Search for one matching named-type for success, otherwise return false - // A named-type can be either a class-name or a built-in type like string, int, array, etc. 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 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) { - return true; + if ($isTypeUnion) { + return true; + } + } else { + if (!$isTypeUnion) { + return false; + } } } - return false; + // 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; } diff --git a/tests/FunctionCheckTypehintTest.php b/tests/FunctionCheckTypehintTest.php index b6f8aef5..b263473e 100644 --- a/tests/FunctionCheckTypehintTest.php +++ b/tests/FunctionCheckTypehintTest.php @@ -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) { diff --git a/tests/fixtures/CallbackWithIntersectionTypehintClass.php b/tests/fixtures/CallbackWithIntersectionTypehintClass.php new file mode 100644 index 00000000..3e733476 --- /dev/null +++ b/tests/fixtures/CallbackWithIntersectionTypehintClass.php @@ -0,0 +1,21 @@ +