Repository: MarcinOrlowski/laravel-api-response-builder (php)

Issue:
Validator: Missing check for InvalidTypeExceptionContract
Missing Contract Check in Validator: src/Validator.php has FIXME comments indicating it doesn't validate the provided exception class against the required contract (InvalidTypeExceptionContract).

Pull request description:
Fix(Validator): Ensure assertIsType validates exception contract (GH-263)
Closes #263

Patch:
diff --git a/docs/CHANGES.md b/docs/CHANGES.md
index 39dbcdf..696d3a0 100644
--- a/docs/CHANGES.md
+++ b/docs/CHANGES.md
@@ -18,6 +18,10 @@ should be able to easily backport future new features to older versions rather e
 
 ## CHANGE LOG ##
 
+* @dev
+  * [GH-263] Fixed `Validator::assertIsType()` not validating provided exception class against `InvalidTypeExceptionContract`.
+  * [GH-263] Added unit tests for `Validator::assertIsType()`.
+
 * v12.0.1 (2025-04-16)
   * [RB-255] Fixed `ToArrayConverter` using a new Request instance instead of the actual request.
   * [BR-256] Fixed `ToArrayConverter` potentially causing fatal error if object lacks `toArray` method.
diff --git a/src/Validator.php b/src/Validator.php
index 38715f7..a91f945 100644
--- a/src/Validator.php
+++ b/src/Validator.php
@@ -156,6 +156,13 @@ final class Validator
             throw new \InvalidArgumentException('The $allowed_types array cannot be empty.');
         }
 
+        // Ensure the provided exception class implements the required contract
+        if (!is_subclass_of($ex_class, InvalidTypeExceptionContract::class)) {
+            throw new \InvalidArgumentException(
+                sprintf('Exception class "%s" must implement "%s".', $ex_class, InvalidTypeExceptionContract::class)
+            );
+        }
+
         // Type::EXISTING_CLASS is artificial type, so we need separate logic to handle it.
         $tmp = $allowed_types;
         $idx = \array_search(Type::EXISTING_CLASS, $tmp, true);
@@ -173,13 +180,14 @@ final class Validator
 
         if (!empty($tmp)) {
             if (!\in_array($value_type, $allowed_types, true)) {
-                // FIXME we need to ensure $ex_class implements InvalidTypeExceptionContract at some point.
-                /** @var \Exception $ex_class */
+                /** @var InvalidTypeExceptionContract $ex_class */
                 throw new $ex_class($var_name, $value_type, $allowed_types);
             }
         } else {
-            // FIXME we need to ensure $ex_class implements InvalidTypeExceptionContract at some point.
-            throw new Ex\ClassNotFound($var_name, $value_type, $allowed_types);
+            // This case implies only Type::EXISTING_CLASS was allowed, but the class check failed earlier.
+            // We still need to throw an exception that adheres to the contract.
+            /** @var InvalidTypeExceptionContract $ex_class */
+            throw new $ex_class($var_name, $value_type, $allowed_types);
         }
     }
 


Test patch:
diff --git a/tests/phpunit/Validator/ValidatorTest.php b/tests/phpunit/Validator/ValidatorTest.php
index d179903..1a61970 100644
--- a/tests/phpunit/Validator/ValidatorTest.php
+++ b/tests/phpunit/Validator/ValidatorTest.php
@@ -255,4 +255,48 @@ class ValidatorTest extends TestCase
         }
     }
 
+    /**
+     * Tests if assertIsType() throws InvalidArgumentException when provided
+     * exception class does not implement InvalidTypeExceptionContract.
+     */
+    public function testAssertIsTypeWithInvalidExceptionClass(): void
+    {
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessageMatches('/must implement/');
+
+        // \stdClass does not implement the required contract
+        Validator::assertIsType('var_name', 'string', [Type::INTEGER], \stdClass::class);
+    }
+
+    /**
+     * Tests if assertIsType() throws the specified custom exception (that implements the contract)
+     * when the type check fails.
+     */
+    public function testAssertIsTypeWithValidCustomExceptionOnFailure(): void
+    {
+        // Use a valid exception class from the project that implements the contract
+        $customExceptionClass = Ex\NotIntegerException::class;
+
+        $this->expectException($customExceptionClass);
+
+        Validator::assertIsType('var_name', 'string', [Type::INTEGER], $customExceptionClass);
+    }
+
+    /**
+     * Tests if assertIsType() passes silently when a valid custom exception class (that implements the contract)
+     * is provided and the type check succeeds.
+     */
+    public function testAssertIsTypeWithValidCustomExceptionOnSuccess(): void
+    {
+        // Use a valid exception class from the project that implements the contract
+        $customExceptionClass = Ex\NotIntegerException::class;
+
+        Validator::assertIsType('var_name', 123, [Type::INTEGER], $customExceptionClass);
+
+        // If no exception is thrown, the test passes.
+        $this->assertTrue(true);
+    }
+
+    // ---------------------------------------------------------------------------------------------
+
 } // end of class