From 5d11fb4db2fe82efd4fff8f0613cef1008a80729 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 2 Oct 2026 00:14:35 +0000 Subject: [PATCH 01/13] Enable PHPStan checked-exception analysis Turn on exceptions.check.missingCheckedExceptionInThrows so PHPStan reports a checked exception that a method throws but does not declare in its @throws tag. Tests are excluded, because PHPUnit handles any exception a test throws. Configure Error and LogicException as unchecked. They signal programmer errors, such as mixing the $values array with named arguments. Setting them explicitly also makes the result independent of the PHPStan version, because older 2.2 releases treat Error as checked by default. Declare the InvalidInputException that the request methods and their private validation helpers throw when input validation is enabled. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 ++++ phpstan.neon | 12 ++++++ src/MinFraud.php | 75 ++++++++++++++++++++++++++++++---- src/MinFraud/ServiceClient.php | 9 ++++ 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 465f25fd..148dc6c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ CHANGELOG ========= +3.8.0 +------------------ + +* The PHPDoc for the `with()` and `with*()` methods of `MaxMind\MinFraud` now + lists the `InvalidInputException` they throw when input validation is + enabled. + 3.7.0 (2026-07-21) ------------------ diff --git a/phpstan.neon b/phpstan.neon index 63aadbac..302e6efb 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,3 +3,15 @@ parameters: paths: - src - tests + exceptions: + # These classes signal programmer errors, so callers need not declare them. + uncheckedExceptionClasses: + - Error + - LogicException + check: + missingCheckedExceptionInThrows: true + ignoreErrors: + # PHPUnit handles any exception a test throws, so tests do not declare them. + - + identifier: missingType.checkedException + path: tests/* diff --git a/src/MinFraud.php b/src/MinFraud.php index 49eeebad..67ef75ff 100644 --- a/src/MinFraud.php +++ b/src/MinFraud.php @@ -119,6 +119,10 @@ public function jsonSerialize(): array * * @param array $values The request as a structured array * + * @throws InvalidInputException if input validation is enabled and a + * value is invalid, a value has the wrong + * type, or a key is unknown + * * @return MinFraud A new immutable MinFraud object. This object is * a clone of the original with additional data. */ @@ -194,11 +198,15 @@ public function with(array $values): self * Device Tracking Add-on for explicit * device linking * - * @return MinFraud A new immutable MinFraud object. This object is a clone - * of the original with additional data. - * * @link https://dev.maxmind.com/minfraud/api-documentation/requests/?lang=en#schema--request--device * minFraud device API docs + * + * @throws InvalidInputException if input validation is enabled and a + * value is invalid, a value has the wrong + * type, or a key is unknown + * + * @return MinFraud A new immutable MinFraud object. This object is a clone + * of the original with additional data. */ public function withDevice( array $values = [], @@ -321,11 +329,15 @@ public function withDevice( * - `agent` * - `customer` * - * @return MinFraud A new immutable MinFraud object. This object is a clone of - * the original with additional data. - * * @link https://dev.maxmind.com/minfraud/api-documentation/requests/?lang=en#schema--request--event * minFraud event API docs + * + * @throws InvalidInputException if input validation is enabled and a + * value is invalid, a value has the wrong + * type, or a key is unknown + * + * @return MinFraud A new immutable MinFraud object. This object is a clone of + * the original with additional data. */ public function withEvent( array $values = [], @@ -417,6 +429,10 @@ public function withEvent( * the username or login name associated * with the account * + * @throws InvalidInputException if input validation is enabled and a + * value is invalid, a value has the wrong + * type, or a key is unknown + * * @return MinFraud A new immutable MinFraud object. This object is a clone * of the original with additional data. */ @@ -470,6 +486,10 @@ public function withAccount( * transaction. Do not include the `@` in this * field. * + * @throws InvalidInputException if input validation is enabled and a + * value is invalid, a value has the wrong + * type, or a key is unknown + * * @return MinFraud A new immutable MinFraud object. This object is a clone * of the original with additional data. */ @@ -550,6 +570,10 @@ public function withEmail( * @param string|null $region The ISO 3166-2 subdivision code for the user's * billing address * + * @throws InvalidInputException if input validation is enabled and a + * value is invalid, a value has the wrong + * type, or a key is unknown + * * @return MinFraud A new immutable MinFraud object. This object is a clone * of the original with additional data. */ @@ -662,6 +686,10 @@ public function withBilling( * code of the user's shipping address * @param string|null $postal The postal code of the user's shipping address * + * @throws InvalidInputException if input validation is enabled and a + * value is invalid, a value has the wrong + * type, or a key is unknown + * * @return MinFraud A new immutable MinFraud object. This object is * a clone of the original with additional data. */ @@ -794,6 +822,10 @@ public function withShipping( * - `real_time_payment` * - `rewards` * + * @throws InvalidInputException if input validation is enabled and a + * value is invalid, a value has the wrong + * type, or a key is unknown + * * @return MinFraud A new immutable MinFraud object. This object is * a clone of the original with additional data. */ @@ -1059,6 +1091,10 @@ public function withPayment( * @param bool|null $was3dSecureSuccessful Whether the outcome of 3-D Secure * verification was successful * + * @throws InvalidInputException if input validation is enabled and a + * value is invalid, a value has the wrong + * type, or a key is unknown + * * @return MinFraud A new immutable MinFraud object. This object is a clone of * the original with additional data. */ @@ -1179,6 +1215,9 @@ public function withCreditCard( * * @param array $values the custom inputs to send in the request * + * @throws InvalidInputException if input validation is enabled and a + * key or value is invalid + * * @return MinFraud A new immutable MinFraud object. This object is * a clone of the original with additional data. */ @@ -1241,9 +1280,13 @@ public function withCustomInputs(array $values): self * @param string|null $subaffiliateId The ID of the sub-affiliate where the order is coming from. * No specific format is required. * - * @return MinFraud A new immutable MinFraud object. This object is a clone of the original with additional data. - * * @see https://support.maxmind.com/knowledge-base/articles/order-and-shopping-cart-inputs-minfraud + * + * @throws InvalidInputException if input validation is enabled and a + * value is invalid, a value has the wrong + * type, or a key is unknown + * + * @return MinFraud A new immutable MinFraud object. This object is a clone of the original with additional data. */ public function withOrder( array $values = [], @@ -1340,6 +1383,10 @@ public function withOrder( * @param int|null $quantity The quantity of the item in the shopping cart. * The value must be a whole number. * + * @throws InvalidInputException if input validation is enabled and a + * value is invalid, a value has the wrong + * type, or a key is unknown + * * @return MinFraud A new immutable MinFraud object. This object is a clone * of the original with additional data. */ @@ -1496,6 +1543,10 @@ private function post(string $class, string $path) ); } + /** + * @throws InvalidInputException if the value is invalid and input + * validation is enabled + */ private function verifyCountryCode(string $country): void { if (!preg_match('/^[A-Z]{2}$/', $country)) { @@ -1503,6 +1554,10 @@ private function verifyCountryCode(string $country): void } } + /** + * @throws InvalidInputException if the value is invalid and input + * validation is enabled + */ private function verifyPhoneCountryCode(string $phoneCountryCode): void { if (!preg_match('/^[0-9]{1,4}$/', $phoneCountryCode)) { @@ -1510,6 +1565,10 @@ private function verifyPhoneCountryCode(string $phoneCountryCode): void } } + /** + * @throws InvalidInputException if the value is invalid and input + * validation is enabled + */ private function verifyRegionCode(string $region): void { if (!preg_match('/^[0-9A-Z]{1,4}$/', $region)) { diff --git a/src/MinFraud/ServiceClient.php b/src/MinFraud/ServiceClient.php index b1b6e51e..bb7babbb 100644 --- a/src/MinFraud/ServiceClient.php +++ b/src/MinFraud/ServiceClient.php @@ -60,6 +60,9 @@ protected function userAgent(): string return 'minFraud-API/' . self::VERSION; } + /** + * @throws InvalidInputException if input validation is enabled + */ protected function maybeThrowInvalidInputException(string $msg): void { if ($this->validateInput) { @@ -73,6 +76,9 @@ protected function maybeThrowInvalidInputException(string $msg): void * @param array $array the parent array * @param string $key the key to remove * @param list $types the expected types + * + * @throws InvalidInputException if the value has an unexpected type and + * input validation is enabled */ protected function remove(array &$array, string $key, array $types = ['string']): mixed { @@ -94,6 +100,9 @@ protected function remove(array &$array, string $key, array $types = ['string']) /** * @param array $values + * + * @throws InvalidInputException if the array has unknown keys and input + * validation is enabled */ protected function verifyEmpty(array $values): void { From c65e3ee32b4362ac8c694a15b418736670feaf06 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 2 Oct 2026 14:42:29 +0000 Subject: [PATCH 02/13] Test the $values and named-argument guard The with*() methods and ReportTransaction::report() throw InvalidArgumentException if $values is non-empty and named arguments are provided. No test covered this. Also add withDevice to the withMethods data provider, so the unknown-key test covers it too. Co-Authored-By: Claude Opus 5.5 --- .../ReportTransaction/ReportTransactionTest.php | 12 ++++++++++++ tests/MaxMind/Test/MinFraudTest.php | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/tests/MaxMind/Test/MinFraud/ReportTransaction/ReportTransactionTest.php b/tests/MaxMind/Test/MinFraud/ReportTransaction/ReportTransactionTest.php index 979a079f..32af1e88 100644 --- a/tests/MaxMind/Test/MinFraud/ReportTransaction/ReportTransactionTest.php +++ b/tests/MaxMind/Test/MinFraud/ReportTransaction/ReportTransactionTest.php @@ -137,6 +137,18 @@ public static function requestsMissingRequiredFields(): array ]; } + public function testValuesWithNamedArgs(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('not both'); + + $req = Data::minimalRequest(); + $this->createReportTransactionRequest( + $req, + 0 + )->report($req, notes: 'some notes'); + } + public function testUnknownKey(): void { $this->expectException(InvalidInputException::class); diff --git a/tests/MaxMind/Test/MinFraudTest.php b/tests/MaxMind/Test/MinFraudTest.php index ee9eeeff..5a98c930 100644 --- a/tests/MaxMind/Test/MinFraudTest.php +++ b/tests/MaxMind/Test/MinFraudTest.php @@ -436,12 +436,27 @@ public function testUnknownKeys(string $method): void )->{$method}(['unknown' => 'some value']); } + /** + * @dataProvider withMethods + */ + public function testValuesWithNamedArgs(string $method): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('not both'); + + $this->createMinFraudRequestWithFullResponse( + 'insights', + 0 + )->{$method}(['unknown' => 'some value'], null); + } + /** * @return array> */ public static function withMethods(): array { return [ + ['withDevice'], ['withEvent'], ['withAccount'], ['withEmail'], From 3a8c92346f8e546139a35c34004c0c7e63f69d9d Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 2 Oct 2026 14:42:56 +0000 Subject: [PATCH 03/13] Set minimum versions for dev tools composer.lock is not committed, so the require-dev constraints alone decide which tool versions CI and developers install. PHPStan and PHP_CodeSniffer used "*", which allows any version, including a future major release that changes behavior. php-cs-fixer allowed any 3.x release. Require the major and minor versions that CI installs now: PHPStan 2.2, PHP_CodeSniffer 4.0, and php-cs-fixer 3.95. Co-Authored-By: Claude Opus 5.5 --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 69509101..cbf54b06 100644 --- a/composer.json +++ b/composer.json @@ -22,10 +22,10 @@ "geoip2/geoip2": "^v3.4.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "3.*", + "friendsofphp/php-cs-fixer": "^3.95", "phpunit/phpunit": "^10.0", - "squizlabs/php_codesniffer": "*", - "phpstan/phpstan": "*" + "squizlabs/php_codesniffer": "^4.0", + "phpstan/phpstan": "^2.2" }, "suggest": { "ext-intl": "Enables improved email address IDN normalization" From a927f7a7c7359ce17cceda2e0ac9f9eea6cba4cf Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:01:52 -0700 Subject: [PATCH 04/13] Allow source-only analysis without unmatched PHPUnit ignores --- phpstan.neon | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index 302e6efb..e433e52f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -11,7 +11,8 @@ parameters: check: missingCheckedExceptionInThrows: true ignoreErrors: - # PHPUnit handles any exception a test throws, so tests do not declare them. + # PHPUnit handles exceptions from test methods and their helpers. - identifier: missingType.checkedException path: tests/* + reportUnmatched: false From cf37a21a32b3b8110ea275e3367ea80ab2a8b3a2 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:01:54 -0700 Subject: [PATCH 05/13] Reject malformed request sections and shopping cart items --- CHANGELOG.md | 2 ++ src/MinFraud.php | 22 ++++++++++++++++--- tests/MaxMind/Test/MinFraudTest.php | 33 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 148dc6c2..3039a574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ CHANGELOG 3.8.0 ------------------ +* `with()` now rejects non-array sections and shopping cart items with + `InvalidInputException`, including when input validation is disabled. * The PHPDoc for the `with()` and `with*()` methods of `MaxMind\MinFraud` now lists the `InvalidInputException` they throw when input validation is enabled. diff --git a/src/MinFraud.php b/src/MinFraud.php index 67ef75ff..b04bb09c 100644 --- a/src/MinFraud.php +++ b/src/MinFraud.php @@ -119,15 +119,31 @@ public function jsonSerialize(): array * * @param array $values The request as a structured array * - * @throws InvalidInputException if input validation is enabled and a - * value is invalid, a value has the wrong - * type, or a key is unknown + * @throws InvalidInputException if a section or shopping cart item is not + * an array, or if validation is enabled and + * a value is invalid or a key is unknown * * @return MinFraud A new immutable MinFraud object. This object is * a clone of the original with additional data. */ public function with(array $values): self { + foreach ([ + 'account', 'billing', 'credit_card', 'custom_inputs', 'device', + 'email', 'event', 'order', 'payment', 'shipping', 'shopping_cart', + ] as $section) { + if (\array_key_exists($section, $values) && !\is_array($values[$section])) { + throw new InvalidInputException("The $section section must be an array."); + } + } + if (isset($values['shopping_cart'])) { + foreach ($values['shopping_cart'] as $item) { + if (!\is_array($item)) { + throw new InvalidInputException('Each shopping_cart item must be an array.'); + } + } + } + $new = $this; if (\array_key_exists('account', $values)) { $new = $new->withAccount($this->remove($values, 'account', ['array'])); diff --git a/tests/MaxMind/Test/MinFraudTest.php b/tests/MaxMind/Test/MinFraudTest.php index 5a98c930..49764bc4 100644 --- a/tests/MaxMind/Test/MinFraudTest.php +++ b/tests/MaxMind/Test/MinFraudTest.php @@ -16,6 +16,39 @@ */ class MinFraudTest extends ServiceClientTester { + /** + * @dataProvider invalidRequestStructures + * + * @param array $values + */ + public function testInvalidRequestStructure(array $values, bool $validate): void + { + $client = new MinFraud(1, 'key', ['validateInput' => $validate]); + $this->expectException(InvalidInputException::class); + $this->expectExceptionMessage('must be an array'); + $client->with($values); + } + + /** + * @return array, bool}> + */ + public static function invalidRequestStructures(): array + { + $cases = []; + foreach ([true, false] as $validate) { + foreach ([ + ['device' => null], + ['device' => 'x'], + ['shopping_cart' => null], + ['shopping_cart' => ['x']], + ] as $values) { + $cases[] = [$values, $validate]; + } + } + + return $cases; + } + public function testMinFraud(): void { $minFraud = new MinFraud(0, '', ['hashEmail' => true, 'locales' => ['en', 'fr']]); From 12d088406af8e1840b64d7ad5e4e192e6f938ba1 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:01:56 -0700 Subject: [PATCH 06/13] Validate custom input types only when validation is enabled --- CHANGELOG.md | 3 ++ src/MinFraud.php | 49 +++++++++++++++-------------- tests/MaxMind/Test/MinFraudTest.php | 32 +++++++++++++++++++ 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3039a574..c66b2492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ CHANGELOG * `with()` now rejects non-array sections and shopping cart items with `InvalidInputException`, including when input validation is disabled. +* Custom input validation now rejects integer keys and array or object values + with `InvalidInputException` instead of a type error or conversion warning. + When validation is disabled, these values pass through unchanged. * The PHPDoc for the `with()` and `with*()` methods of `MaxMind\MinFraud` now lists the `InvalidInputException` they throw when input validation is enabled. diff --git a/src/MinFraud.php b/src/MinFraud.php index b04bb09c..3fd2f55a 100644 --- a/src/MinFraud.php +++ b/src/MinFraud.php @@ -1229,7 +1229,7 @@ public function withCreditCard( * This returns a `MinFraud` object with the `custom_inputs` array set to * `$values`. Existing `custom_inputs` data will be replaced. * - * @param array $values the custom inputs to send in the request + * @param array $values the custom inputs to send in the request * * @throws InvalidInputException if input validation is enabled and a * key or value is invalid @@ -1239,34 +1239,37 @@ public function withCreditCard( */ public function withCustomInputs(array $values): self { - foreach ($values as $key => $value) { - if (\is_string($value)) { - if (str_contains($value, "\n")) { + if ($this->validateInput) { + foreach ($values as $key => $value) { + if (\is_string($value)) { + if (str_contains($value, "\n")) { + $this->maybeThrowInvalidInputException( + "$value is invalid. String custom input values must not contain newline characters.", + ); + } + if ($value === '' || \strlen($value) > 255) { + $this->maybeThrowInvalidInputException( + "$value is invalid. String custom input values must have a length between 1 and 255.", + ); + } + } elseif (is_numeric($value)) { + if ($value < -1e13 + 1 || $value > 1e13 - 1) { + $this->maybeThrowInvalidInputException( + "$value is invalid. Numeric custom input values must be between -1e13 and 1e13.", + ); + } + } elseif (!\is_bool($value)) { $this->maybeThrowInvalidInputException( - "$value is invalid. String custom input values must not contain newline characters.", + 'Custom input values must be strings, numbers, or booleans. Received ' + . get_debug_type($value) . '.', ); } - if ($value === '' || \strlen($value) > 255) { - $this->maybeThrowInvalidInputException( - "$value is invalid. String custom input values must have a length between 1 and 255.", - ); - } - } elseif (is_numeric($value)) { - if ($value < -1e13 + 1 || $value > 1e13 - 1) { + + if (!\is_string($key) || !preg_match('/^[a-z0-9_]{1,25}\Z/', $key)) { $this->maybeThrowInvalidInputException( - "$value is invalid. Numeric custom input values must be between -1e13 and 1e13.", + "$key is invalid. Custom input keys must be alphanumeric and have 25 characters or less.", ); } - } elseif (!\is_bool($value)) { - $this->maybeThrowInvalidInputException( - "$value is invalid. Custom input values must be strings, numbers, or booleans.", - ); - } - - if (!preg_match('/^[a-z0-9_]{1,25}\Z/', $key)) { - $this->maybeThrowInvalidInputException( - "$key is invalid. Custom input keys must be alphanumeric and have 25 characters or less.", - ); } } diff --git a/tests/MaxMind/Test/MinFraudTest.php b/tests/MaxMind/Test/MinFraudTest.php index 49764bc4..7605230d 100644 --- a/tests/MaxMind/Test/MinFraudTest.php +++ b/tests/MaxMind/Test/MinFraudTest.php @@ -49,6 +49,38 @@ public static function invalidRequestStructures(): array return $cases; } + /** + * @dataProvider invalidCustomInputs + * + * @param array $values + */ + public function testInvalidCustomInput(array $values): void + { + $client = new MinFraud(1, 'key'); + $this->expectException(InvalidInputException::class); + $client->withCustomInputs($values); + } + + /** + * @dataProvider invalidCustomInputs + * + * @param array $values + */ + public function testCustomInputsWithoutValidation(array $values): void + { + $client = new MinFraud(1, 'key', ['validateInput' => false]); + $result = $client->withCustomInputs($values); + $this->assertSame($values, $result->jsonSerialize()['content']['custom_inputs']); + } + + /** + * @return array}> + */ + public static function invalidCustomInputs(): array + { + return [[['value']], [['a' => new \stdClass()]], [['a' => ['x']]]]; + } + public function testMinFraud(): void { $minFraud = new MinFraud(0, '', ['hashEmail' => true, 'locales' => ['en', 'fr']]); From d50ffa2f7ddb4d73f082c0e639858b2a1a55932f Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:01:59 -0700 Subject: [PATCH 07/13] Skip address field validators when input validation is disabled --- CHANGELOG.md | 4 ++++ src/MinFraud.php | 30 +++++++++++++++++++++-------- tests/MaxMind/Test/MinFraudTest.php | 27 ++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c66b2492..ef3d79f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ CHANGELOG * Custom input validation now rejects integer keys and array or object values with `InvalidInputException` instead of a type error or conversion warning. When validation is disabled, these values pass through unchanged. +* When input validation is disabled, billing, shipping, and credit card + country fields no longer fail in string validators. Billing and shipping + region and phone country code fields, and credit card bank phone country + codes, also pass through unchanged. * The PHPDoc for the `with()` and `with*()` methods of `MaxMind\MinFraud` now lists the `InvalidInputException` they throw when input validation is enabled. diff --git a/src/MinFraud.php b/src/MinFraud.php index 3fd2f55a..2cf7703d 100644 --- a/src/MinFraud.php +++ b/src/MinFraud.php @@ -646,7 +646,9 @@ public function withBilling( } if ($country !== null) { - $this->verifyCountryCode($country); + if ($this->validateInput) { + $this->verifyCountryCode($country); + } $values['country'] = $country; } @@ -659,7 +661,9 @@ public function withBilling( } if ($phoneCountryCode !== null) { - $this->verifyPhoneCountryCode($phoneCountryCode); + if ($this->validateInput) { + $this->verifyPhoneCountryCode($phoneCountryCode); + } $values['phone_country_code'] = $phoneCountryCode; } @@ -672,7 +676,9 @@ public function withBilling( } if ($region !== null) { - $this->verifyRegionCode($region); + if ($this->validateInput) { + $this->verifyRegionCode($region); + } $values['region'] = $region; } @@ -764,7 +770,9 @@ public function withShipping( } if ($country !== null) { - $this->verifyCountryCode($country); + if ($this->validateInput) { + $this->verifyCountryCode($country); + } $values['country'] = $country; } @@ -785,7 +793,9 @@ public function withShipping( } if ($phoneCountryCode !== null) { - $this->verifyPhoneCountryCode($phoneCountryCode); + if ($this->validateInput) { + $this->verifyPhoneCountryCode($phoneCountryCode); + } $values['phone_country_code'] = $phoneCountryCode; } @@ -798,7 +808,9 @@ public function withShipping( } if ($region !== null) { - $this->verifyRegionCode($region); + if ($this->validateInput) { + $this->verifyRegionCode($region); + } $values['region'] = $region; } @@ -1162,7 +1174,7 @@ public function withCreditCard( } if ($bankPhoneCountryCode !== null) { - if (!preg_match('/^[0-9]{1,4}$/', $bankPhoneCountryCode)) { + if ($this->validateInput && !preg_match('/^[0-9]{1,4}$/', $bankPhoneCountryCode)) { $this->maybeThrowInvalidInputException('Bank phone country code must be a string of 1 to 4 digits.'); } @@ -1174,7 +1186,9 @@ public function withCreditCard( } if ($country !== null) { - $this->verifyCountryCode($country); + if ($this->validateInput) { + $this->verifyCountryCode($country); + } $values['country'] = $country; } diff --git a/tests/MaxMind/Test/MinFraudTest.php b/tests/MaxMind/Test/MinFraudTest.php index 7605230d..382a48a2 100644 --- a/tests/MaxMind/Test/MinFraudTest.php +++ b/tests/MaxMind/Test/MinFraudTest.php @@ -81,6 +81,33 @@ public static function invalidCustomInputs(): array return [[['value']], [['a' => new \stdClass()]], [['a' => ['x']]]]; } + /** + * @dataProvider unvalidatedAddressFields + */ + public function testAddressFieldWithoutValidation(string $method, string $section, string $field): void + { + $client = new MinFraud(1, 'key', ['validateInput' => false]); + $result = $client->{$method}([$field => 12]); + $this->assertSame(12, $result->jsonSerialize()['content'][$section][$field]); + } + + /** + * @return array + */ + public static function unvalidatedAddressFields(): array + { + return [ + ['withBilling', 'billing', 'country'], + ['withBilling', 'billing', 'region'], + ['withBilling', 'billing', 'phone_country_code'], + ['withShipping', 'shipping', 'country'], + ['withShipping', 'shipping', 'region'], + ['withShipping', 'shipping', 'phone_country_code'], + ['withCreditCard', 'credit_card', 'country'], + ['withCreditCard', 'credit_card', 'bank_phone_country_code'], + ]; + } + public function testMinFraud(): void { $minFraud = new MinFraud(0, '', ['hashEmail' => true, 'locales' => ['en', 'fr']]); From 21a072e21e5a37eb32368a18c5c7479d024b9a4d Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:02:01 -0700 Subject: [PATCH 08/13] Document setup failures across supported HTTP client versions --- src/MinFraud.php | 7 +++++++ src/MinFraud/ReportTransaction.php | 4 ++++ src/MinFraud/ServiceClient.php | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/src/MinFraud.php b/src/MinFraud.php index 2cf7703d..4cbe70a1 100644 --- a/src/MinFraud.php +++ b/src/MinFraud.php @@ -81,6 +81,9 @@ class MinFraud extends MinFraud\ServiceClient implements \JsonSerializable * to the `with*()` methods are validated. It is recommended that you * leave validation on while developing and only (optionally) disable it * before deployment. + * + * @throws \RuntimeException with older web-service-common releases, if HTTP client setup fails + * @throws WebServiceException if HTTP client setup fails */ public function __construct( int $accountId, @@ -1493,6 +1496,7 @@ public function withShoppingCartItem( * @throws InvalidRequestException when the request is invalid for some * other reason, e.g., invalid JSON in the POST. * @throws HttpException when an unexpected HTTP error occurs + * @throws \RuntimeException with older web-service-common releases, if cURL setup fails * @throws WebServiceException when some other error occurs. This also * serves as the base class for the above exceptions. * @@ -1515,6 +1519,7 @@ public function score(): Score * @throws InvalidRequestException when the request is invalid for some * other reason, e.g., invalid JSON in the POST. * @throws HttpException when an unexpected HTTP error occurs + * @throws \RuntimeException with older web-service-common releases, if cURL setup fails * @throws WebServiceException when some other error occurs. This also * serves as the base class for the above exceptions. * @@ -1537,6 +1542,7 @@ public function insights(): Insights * @throws InvalidRequestException when the request is invalid for some * other reason, e.g., invalid JSON in the POST. * @throws HttpException when an unexpected HTTP error occurs + * @throws \RuntimeException with older web-service-common releases, if cURL setup fails * @throws WebServiceException when some other error occurs. This also * serves as the base class for the above exceptions. * @@ -1559,6 +1565,7 @@ public function factors(): Factors * @throws InvalidRequestException when the request is invalid for some * other reason, e.g., invalid JSON in the POST. * @throws HttpException when an unexpected HTTP error occurs + * @throws \RuntimeException with older web-service-common releases, if cURL setup fails * @throws WebServiceException when some other error occurs. This also * serves as the base class for the above exceptions. * diff --git a/src/MinFraud/ReportTransaction.php b/src/MinFraud/ReportTransaction.php index 39072ba6..99f94cd4 100644 --- a/src/MinFraud/ReportTransaction.php +++ b/src/MinFraud/ReportTransaction.php @@ -29,6 +29,9 @@ class ReportTransaction extends ServiceClient * to the `with*()` methods are validated. It is recommended that you * leave validation on while developing and only (optionally) disable it * before deployment. + * + * @throws \RuntimeException with older web-service-common releases, if HTTP client setup fails + * @throws WebServiceException if HTTP client setup fails */ public function __construct( int $accountId, @@ -90,6 +93,7 @@ public function __construct( * other reason, e.g., invalid JSON in the * POST. * @throws HttpException when an unexpected HTTP error occurs + * @throws \RuntimeException with older web-service-common releases, if cURL setup fails * @throws WebServiceException when some other error occurs. This also * serves as the base class for the above * exceptions. diff --git a/src/MinFraud/ServiceClient.php b/src/MinFraud/ServiceClient.php index bb7babbb..1699edc3 100644 --- a/src/MinFraud/ServiceClient.php +++ b/src/MinFraud/ServiceClient.php @@ -5,6 +5,7 @@ namespace MaxMind\MinFraud; use MaxMind\Exception\InvalidInputException; +use MaxMind\Exception\WebServiceException; use MaxMind\WebService\Client; abstract class ServiceClient @@ -35,6 +36,9 @@ abstract class ServiceClient * @param int $accountId your account ID * @param string $licenseKey your license key * @param array $options options for the client + * + * @throws \RuntimeException with older web-service-common releases, if HTTP client setup fails + * @throws WebServiceException if HTTP client setup fails */ public function __construct( int $accountId, From 68e7b20a6d2bc92f52c38eef2e810cae91a42798 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:02:03 -0700 Subject: [PATCH 09/13] Exercise named arguments in the mixed-argument tests --- tests/MaxMind/Test/MinFraudTest.php | 47 ++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/tests/MaxMind/Test/MinFraudTest.php b/tests/MaxMind/Test/MinFraudTest.php index 382a48a2..410a90ce 100644 --- a/tests/MaxMind/Test/MinFraudTest.php +++ b/tests/MaxMind/Test/MinFraudTest.php @@ -528,20 +528,6 @@ public function testUnknownKeys(string $method): void )->{$method}(['unknown' => 'some value']); } - /** - * @dataProvider withMethods - */ - public function testValuesWithNamedArgs(string $method): void - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('not both'); - - $this->createMinFraudRequestWithFullResponse( - 'insights', - 0 - )->{$method}(['unknown' => 'some value'], null); - } - /** * @return array> */ @@ -561,6 +547,39 @@ public static function withMethods(): array ]; } + /** + * @dataProvider withNamedArguments + */ + public function testValuesWithNamedArgs(string $method, string $parameter): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('not both'); + + $this->createMinFraudRequestWithFullResponse( + 'insights', + 0 + )->{$method}(['unknown' => 'some value'], ...[$parameter => 'value']); + } + + /** + * @return array + */ + public static function withNamedArguments(): array + { + return [ + ['withDevice', 'userAgent'], + ['withEvent', 'transactionId'], + ['withAccount', 'userId'], + ['withEmail', 'domain'], + ['withBilling', 'country'], + ['withShipping', 'country'], + ['withPayment', 'processor'], + ['withCreditCard', 'country'], + ['withOrder', 'currency'], + ['withShoppingCartItem', 'itemId'], + ]; + } + /** * @dataProvider badMd5s */ From 417e5e775f7d9d7dba46819add7131c0d6e537a3 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:02:05 -0700 Subject: [PATCH 10/13] Document missing shipping and shopping cart parameters --- src/MinFraud.php | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/MinFraud.php b/src/MinFraud.php index 4cbe70a1..ba94763b 100644 --- a/src/MinFraud.php +++ b/src/MinFraud.php @@ -698,18 +698,25 @@ public function withBilling( * @link https://dev.maxmind.com/minfraud/api-documentation/requests/?lang=en#schema--request--shipping * minFraud shipping API docs * - * @param array $values An array of shipping data. The keys are the same as - * the JSON keys. You may use either this or the named - * arguments, but not both. - * @param string|null $company The company of the end user as provided in - * their shipping information - * @param string|null $address The first line of the user's shipping address - * @param string|null $city The city of the user's shipping address - * @param string|null $region The ISO 3166-2 subdivision code for the user's - * shipping address - * @param string|null $country The two character ISO 3166-1 alpha-2 country - * code of the user's shipping address - * @param string|null $postal The postal code of the user's shipping address + * @param array $values An array of shipping data. The keys are the same as + * the JSON keys. You may use either this or the named + * arguments, but not both. + * @param string|null $company The company of the end user as provided in + * their shipping information + * @param string|null $address The first line of the user's shipping address + * @param string|null $city The city of the user's shipping address + * @param string|null $region The ISO 3166-2 subdivision code for the user's + * shipping address + * @param string|null $country The two character ISO 3166-1 alpha-2 country + * code of the user's shipping address + * @param string|null $postal The postal code of the user's shipping address + * @param string|null $address2 the second line of the shipping address + * @param string|null $deliverySpeed the delivery speed: same_day, overnight, + * expedited, or standard + * @param string|null $firstName the recipient's first name + * @param string|null $lastName the recipient's last name + * @param string|null $phoneCountryCode the phone country code, with 1 to 4 digits + * @param string|null $phoneNumber the recipient's phone number * * @throws InvalidInputException if input validation is enabled and a * value is invalid, a value has the wrong @@ -1418,6 +1425,7 @@ public function withOrder( * order currency. * @param int|null $quantity The quantity of the item in the shopping cart. * The value must be a whole number. + * @param string|null $itemId the identifier of the item in the shopping cart * * @throws InvalidInputException if input validation is enabled and a * value is invalid, a value has the wrong From 166ada58048fbb6c14050e9c540d77aa2d74f604 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:02:07 -0700 Subject: [PATCH 11/13] Name the account data replaced by withAccount --- src/MinFraud.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MinFraud.php b/src/MinFraud.php index ba94763b..29b210db 100644 --- a/src/MinFraud.php +++ b/src/MinFraud.php @@ -434,7 +434,7 @@ public function withEvent( /** * This returns a `MinFraud` object with the `account` array set to - * the values provided. Existing `` data will be replaced. + * the values provided. Existing `account` data will be replaced. * * @link https://dev.maxmind.com/minfraud/api-documentation/requests/?lang=en#schema--request--account * minFraud account API docs From 46518c1812c3dfb8da7027ab3c28cb6d876e965b Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:02:09 -0700 Subject: [PATCH 12/13] Require exception declarations in validation guidance --- CLAUDE.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 46ba4675..5f386ca8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -240,7 +240,7 @@ When adding a new input field to a `->with*()` method: return $new; ``` -5. **Update PHPDoc** with full documentation +5. **Update PHPDoc**, including `@throws InvalidInputException` on validation methods and their callers. 6. **Add tests** for the new field 7. **Update CHANGELOG.md** @@ -289,6 +289,9 @@ When adding input validation: 1. **Create a validation method** following the pattern: ```php + /** + * @throws InvalidInputException if validation is enabled and the value is invalid + */ private function verifyFieldName(string $value): void { if (!preg_match('/pattern/', $value)) { From 25e73a76ad2fd48a2654efbe96b9661b7f0317e7 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:50:19 -0700 Subject: [PATCH 13/13] Correct the web service exception inheritance descriptions --- src/MinFraud.php | 12 ++++-------- src/MinFraud/ReportTransaction.php | 4 +--- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/MinFraud.php b/src/MinFraud.php index 29b210db..7b17b330 100644 --- a/src/MinFraud.php +++ b/src/MinFraud.php @@ -1505,8 +1505,7 @@ public function withShoppingCartItem( * other reason, e.g., invalid JSON in the POST. * @throws HttpException when an unexpected HTTP error occurs * @throws \RuntimeException with older web-service-common releases, if cURL setup fails - * @throws WebServiceException when some other error occurs. This also - * serves as the base class for the above exceptions. + * @throws WebServiceException when another web service error occurs * * @return Score minFraud Score model object */ @@ -1528,8 +1527,7 @@ public function score(): Score * other reason, e.g., invalid JSON in the POST. * @throws HttpException when an unexpected HTTP error occurs * @throws \RuntimeException with older web-service-common releases, if cURL setup fails - * @throws WebServiceException when some other error occurs. This also - * serves as the base class for the above exceptions. + * @throws WebServiceException when another web service error occurs * * @return Insights minFraud Insights model object */ @@ -1551,8 +1549,7 @@ public function insights(): Insights * other reason, e.g., invalid JSON in the POST. * @throws HttpException when an unexpected HTTP error occurs * @throws \RuntimeException with older web-service-common releases, if cURL setup fails - * @throws WebServiceException when some other error occurs. This also - * serves as the base class for the above exceptions. + * @throws WebServiceException when another web service error occurs * * @return Factors minFraud Factors model object */ @@ -1574,8 +1571,7 @@ public function factors(): Factors * other reason, e.g., invalid JSON in the POST. * @throws HttpException when an unexpected HTTP error occurs * @throws \RuntimeException with older web-service-common releases, if cURL setup fails - * @throws WebServiceException when some other error occurs. This also - * serves as the base class for the above exceptions. + * @throws WebServiceException when another web service error occurs * * @return mixed the model class for the service */ diff --git a/src/MinFraud/ReportTransaction.php b/src/MinFraud/ReportTransaction.php index 99f94cd4..fc8e470f 100644 --- a/src/MinFraud/ReportTransaction.php +++ b/src/MinFraud/ReportTransaction.php @@ -94,9 +94,7 @@ public function __construct( * POST. * @throws HttpException when an unexpected HTTP error occurs * @throws \RuntimeException with older web-service-common releases, if cURL setup fails - * @throws WebServiceException when some other error occurs. This also - * serves as the base class for the above - * exceptions. + * @throws WebServiceException when another web service error occurs */ public function report( array $values = [],