From a37c12db74671a82dcb46af7bee29def2bd32903 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Thu, 1 Oct 2026 23:56:51 +0000 Subject: [PATCH 01/12] Enable PHPStan checked-exception analysis Turn on missingCheckedExceptionInThrows. Tests stay in the analysis for all other rules, but missing @throws tags in tests are ignored, because PHPUnit handles any exception a test throws. Configure Error and BadMethodCallException as unchecked. They signal programmer errors. Setting Error explicitly also makes the result independent of the PHPStan version, because older 2.2 releases treat Error as checked by default. InvalidArgumentException stays checked, because the lookup methods throw it for an invalid IP address, which is runtime data. Document the exceptions that Database\Reader passes on from MaxMind\Db\Reader: - The constructor can also throw UnexpectedValueException. - The lookup methods throw BadMethodCallException for a closed reader or a lookup in progress, and InvalidArgumentException for an IPv6 address in an IPv4-only database. - metadata() declares InvalidDatabaseException, which the C extension throws if it cannot decode the metadata. It no longer declares InvalidArgumentException, which it cannot throw. The private lookup helpers declare their exceptions so that the check follows them to the public methods. Inline ignores cover the exceptions that the code cannot throw. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 18 ++++--- phpstan.neon | 12 +++++ src/Database/Reader.php | 110 ++++++++++++++++++++++++++++++++-------- 3 files changed, 113 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16f2c13d..a3258d6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,18 @@ CHANGELOG ========= -3.4.1 (unreleased) ------------------- - -* The `GeoIp2\Database\Reader` constructor and lookup methods now declare the - `InvalidArgumentException` thrown by `MaxMind\Db\Reader` for a missing or - unreadable database file and for an invalid IP address. +3.5.0 +------------------ + +* The `GeoIp2\Database\Reader` lookup methods now declare the + `InvalidArgumentException` thrown for an invalid IP address and for an IPv6 + address in an IPv4-only database. The constructor now declares it for a + missing or unreadable database file. +* The `GeoIp2\Database\Reader` PHPDoc now lists more exceptions: + `UnexpectedValueException` from the constructor with the pure PHP reader, + and `InvalidDatabaseException` from `metadata()` with the C extension. + `metadata()` no longer declares `InvalidArgumentException`, which it cannot + throw. 3.4.0 (2026-07-16) ------------------ diff --git a/phpstan.neon b/phpstan.neon index 63aadbac..e43f8fb2 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 + - BadMethodCallException + 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/Database/Reader.php b/src/Database/Reader.php index 62bbf189..7d056be3 100644 --- a/src/Database/Reader.php +++ b/src/Database/Reader.php @@ -51,12 +51,18 @@ class Reader implements ProviderInterface /** * Constructor. * + * The declared exceptions come from the pure-PHP reader. The C extension + * may differ. + * * @param string $filename the path to the GeoIP database file * @param array $locales list of locale codes to use in name property * from most preferred to least preferred * * @throws InvalidDatabaseException if the database is corrupt or invalid - * @throws \InvalidArgumentException if the database file does not exist or is not readable + * @throws \InvalidArgumentException if the database file does not exist or + * is not readable + * @throws \UnexpectedValueException if the size of the database file + * cannot be determined */ public function __construct( string $filename, @@ -74,8 +80,12 @@ public function __construct( * * @throws AddressNotFoundException if the address is not in the database * @throws InvalidDatabaseException if the database is corrupt or invalid - * @throws \BadMethodCallException if this database type is not supported - * @throws \InvalidArgumentException if the IP address is not valid + * @throws \BadMethodCallException if the database type does not support + * this method, the reader is closed, or + * a lookup is in progress + * @throws \InvalidArgumentException if the IP address is not valid, or if + * it is an IPv6 address and the database + * is IPv4-only */ public function city(string $ipAddress): City { @@ -89,8 +99,12 @@ public function city(string $ipAddress): City * * @throws AddressNotFoundException if the address is not in the database * @throws InvalidDatabaseException if the database is corrupt or invalid - * @throws \BadMethodCallException if this database type is not supported - * @throws \InvalidArgumentException if the IP address is not valid + * @throws \BadMethodCallException if the database type does not support + * this method, the reader is closed, or + * a lookup is in progress + * @throws \InvalidArgumentException if the IP address is not valid, or if + * it is an IPv6 address and the database + * is IPv4-only */ public function country(string $ipAddress): Country { @@ -104,8 +118,12 @@ public function country(string $ipAddress): Country * * @throws AddressNotFoundException if the address is not in the database * @throws InvalidDatabaseException if the database is corrupt or invalid - * @throws \BadMethodCallException if this database type is not supported - * @throws \InvalidArgumentException if the IP address is not valid + * @throws \BadMethodCallException if the database type does not support + * this method, the reader is closed, or + * a lookup is in progress + * @throws \InvalidArgumentException if the IP address is not valid, or if + * it is an IPv6 address and the database + * is IPv4-only */ public function anonymousIp(string $ipAddress): AnonymousIp { @@ -123,8 +141,12 @@ public function anonymousIp(string $ipAddress): AnonymousIp * * @throws AddressNotFoundException if the address is not in the database * @throws InvalidDatabaseException if the database is corrupt or invalid - * @throws \BadMethodCallException if this database type is not supported - * @throws \InvalidArgumentException if the IP address is not valid + * @throws \BadMethodCallException if the database type does not support + * this method, the reader is closed, or + * a lookup is in progress + * @throws \InvalidArgumentException if the IP address is not valid, or if + * it is an IPv6 address and the database + * is IPv4-only */ public function anonymousPlus(string $ipAddress): AnonymousPlus { @@ -142,8 +164,12 @@ public function anonymousPlus(string $ipAddress): AnonymousPlus * * @throws AddressNotFoundException if the address is not in the database * @throws InvalidDatabaseException if the database is corrupt or invalid - * @throws \BadMethodCallException if this database type is not supported - * @throws \InvalidArgumentException if the IP address is not valid + * @throws \BadMethodCallException if the database type does not support + * this method, the reader is closed, or + * a lookup is in progress + * @throws \InvalidArgumentException if the IP address is not valid, or if + * it is an IPv6 address and the database + * is IPv4-only */ public function asn(string $ipAddress): Asn { @@ -161,8 +187,12 @@ public function asn(string $ipAddress): Asn * * @throws AddressNotFoundException if the address is not in the database * @throws InvalidDatabaseException if the database is corrupt or invalid - * @throws \BadMethodCallException if this database type is not supported - * @throws \InvalidArgumentException if the IP address is not valid + * @throws \BadMethodCallException if the database type does not support + * this method, the reader is closed, or + * a lookup is in progress + * @throws \InvalidArgumentException if the IP address is not valid, or if + * it is an IPv6 address and the database + * is IPv4-only */ public function connectionType(string $ipAddress): ConnectionType { @@ -180,8 +210,12 @@ public function connectionType(string $ipAddress): ConnectionType * * @throws AddressNotFoundException if the address is not in the database * @throws InvalidDatabaseException if the database is corrupt or invalid - * @throws \BadMethodCallException if this database type is not supported - * @throws \InvalidArgumentException if the IP address is not valid + * @throws \BadMethodCallException if the database type does not support + * this method, the reader is closed, or + * a lookup is in progress + * @throws \InvalidArgumentException if the IP address is not valid, or if + * it is an IPv6 address and the database + * is IPv4-only */ public function domain(string $ipAddress): Domain { @@ -199,8 +233,12 @@ public function domain(string $ipAddress): Domain * * @throws AddressNotFoundException if the address is not in the database * @throws InvalidDatabaseException if the database is corrupt or invalid - * @throws \BadMethodCallException if this database type is not supported - * @throws \InvalidArgumentException if the IP address is not valid + * @throws \BadMethodCallException if the database type does not support + * this method, the reader is closed, or + * a lookup is in progress + * @throws \InvalidArgumentException if the IP address is not valid, or if + * it is an IPv6 address and the database + * is IPv4-only */ public function enterprise(string $ipAddress): Enterprise { @@ -214,8 +252,12 @@ public function enterprise(string $ipAddress): Enterprise * * @throws AddressNotFoundException if the address is not in the database * @throws InvalidDatabaseException if the database is corrupt or invalid - * @throws \BadMethodCallException if this database type is not supported - * @throws \InvalidArgumentException if the IP address is not valid + * @throws \BadMethodCallException if the database type does not support + * this method, the reader is closed, or + * a lookup is in progress + * @throws \InvalidArgumentException if the IP address is not valid, or if + * it is an IPv6 address and the database + * is IPv4-only */ public function isp(string $ipAddress): Isp { @@ -226,6 +268,12 @@ public function isp(string $ipAddress): Isp ); } + /** + * @throws AddressNotFoundException + * @throws InvalidDatabaseException + * @throws \BadMethodCallException + * @throws \InvalidArgumentException + */ private function modelFor(string $class, string $type, string $ipAddress): object { [$record, $prefixLen] = $this->getRecord($class, $type, $ipAddress); @@ -236,6 +284,12 @@ private function modelFor(string $class, string $type, string $ipAddress): objec return new $class($record, $this->locales); } + /** + * @throws AddressNotFoundException + * @throws InvalidDatabaseException + * @throws \BadMethodCallException + * @throws \InvalidArgumentException + */ private function flatModelFor(string $class, string $type, string $ipAddress): object { [$record, $prefixLen] = $this->getRecord($class, $type, $ipAddress); @@ -247,11 +301,18 @@ private function flatModelFor(string $class, string $type, string $ipAddress): o } /** + * @throws AddressNotFoundException + * @throws InvalidDatabaseException + * @throws \BadMethodCallException + * @throws \InvalidArgumentException + * * @return array{0:array, 1:int} */ private function getRecord(string $class, string $type, string $ipAddress): array { if (!str_contains($this->dbType, $type)) { + // Every caller passes the ::class constant of a model class. + // @phpstan-ignore missingType.checkedException $method = lcfirst((new \ReflectionClass($class))->getShortName()); throw new \BadMethodCallException( @@ -282,13 +343,17 @@ private function getRecord(string $class, string $type, string $ipAddress): arra } /** - * @throws \InvalidArgumentException if arguments are passed to the method - * @throws \BadMethodCallException if the database has been closed + * @throws \BadMethodCallException if the database has been closed + * @throws InvalidDatabaseException with the C extension, if the metadata + * cannot be decoded * * @return Metadata object for the database */ public function metadata(): Metadata { + // MaxMind\Db\Reader::metadata() declares InvalidArgumentException for + // arguments passed to it, and this call passes none. + // @phpstan-ignore missingType.checkedException return $this->dbReader->metadata(); } @@ -297,6 +362,9 @@ public function metadata(): Metadata */ public function close(): void { + // MaxMind\Db\Reader::close() declares \Exception, but the only + // exception it throws is BadMethodCallException. + // @phpstan-ignore missingType.checkedException $this->dbReader->close(); } } From bd397bcbbac802f57bec9b745145a50685e51016 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 2 Oct 2026 14:49:08 +0000 Subject: [PATCH 02/12] Test lookups on a closed Database\Reader The lookup methods now document the BadMethodCallException that MaxMind\Db\Reader throws for a closed reader. No test covered it. Co-Authored-By: Claude Opus 5.5 --- tests/GeoIp2/Test/Database/ReaderTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/GeoIp2/Test/Database/ReaderTest.php b/tests/GeoIp2/Test/Database/ReaderTest.php index 562eee8c..fb327473 100644 --- a/tests/GeoIp2/Test/Database/ReaderTest.php +++ b/tests/GeoIp2/Test/Database/ReaderTest.php @@ -118,6 +118,16 @@ public function testInvalidAddress(): void $reader->close(); } + public function testClosedReader(): void + { + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('closed'); + + $reader = new Reader('maxmind-db/test-data/GeoIP2-City-Test.mmdb'); + $reader->close(); + $reader->city('81.2.69.160'); + } + public function testAnonymousIp(): void { $reader = new Reader('maxmind-db/test-data/GeoIP2-Anonymous-IP-Test.mmdb'); From 1a77a94753e72dbb6bd5416b773c826e5f8c7381 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 2 Oct 2026 14:49:27 +0000 Subject: [PATCH 03/12] 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 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 3888b994..322c407c 100644 --- a/composer.json +++ b/composer.json @@ -19,10 +19,10 @@ "ext-json": "*" }, "require-dev": { - "friendsofphp/php-cs-fixer": "3.*", + "friendsofphp/php-cs-fixer": "^3.95", "phpunit/phpunit": "^10.0", - "squizlabs/php_codesniffer": "4.*", - "phpstan/phpstan": "*" + "squizlabs/php_codesniffer": "^4.0", + "phpstan/phpstan": "^2.2" }, "autoload": { "psr-4": { From 402b68b3650bcd7f96ad8a716525198af207b0c3 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:00:39 -0700 Subject: [PATCH 04/12] 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 e43f8fb2..7e1d22b9 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 62928b52dc986a42a299849162b4ada6dc6f9343 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:00:40 -0700 Subject: [PATCH 05/12] Keep reader exception ignores compatible across dependency versions --- phpstan.neon | 12 ++++++++++++ src/Database/Reader.php | 6 ------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 7e1d22b9..855be6c8 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -16,3 +16,15 @@ parameters: identifier: missingType.checkedException path: tests/* reportUnmatched: false + # Older reader releases declare exceptions these calls cannot throw. + # Keep these ignores optional until the minimum reader version is raised. + - + message: '#^Method GeoIp2\\Database\\Reader::metadata\(\) throws checked exception InvalidArgumentException but it.s missing from the PHPDoc @throws tag\.$#' + identifier: missingType.checkedException + path: src/Database/Reader.php + reportUnmatched: false + - + message: '#^Method GeoIp2\\Database\\Reader::close\(\) throws checked exception Exception but it.s missing from the PHPDoc @throws tag\.$#' + identifier: missingType.checkedException + path: src/Database/Reader.php + reportUnmatched: false diff --git a/src/Database/Reader.php b/src/Database/Reader.php index 7d056be3..794ecb97 100644 --- a/src/Database/Reader.php +++ b/src/Database/Reader.php @@ -351,9 +351,6 @@ private function getRecord(string $class, string $type, string $ipAddress): arra */ public function metadata(): Metadata { - // MaxMind\Db\Reader::metadata() declares InvalidArgumentException for - // arguments passed to it, and this call passes none. - // @phpstan-ignore missingType.checkedException return $this->dbReader->metadata(); } @@ -362,9 +359,6 @@ public function metadata(): Metadata */ public function close(): void { - // MaxMind\Db\Reader::close() declares \Exception, but the only - // exception it throws is BadMethodCallException. - // @phpstan-ignore missingType.checkedException $this->dbReader->close(); } } From 0f7874b995bb92482d54e9276b600a7e96420a45 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:00:42 -0700 Subject: [PATCH 06/12] Check BadMethodCallException and cover closing a reader twice --- phpstan.neon | 1 - src/Database/Reader.php | 4 ++++ tests/GeoIp2/Test/Database/ReaderTest.php | 10 ++++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index 855be6c8..c861feba 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,7 +7,6 @@ parameters: # These classes signal programmer errors, so callers need not declare them. uncheckedExceptionClasses: - Error - - BadMethodCallException check: missingCheckedExceptionInThrows: true ignoreErrors: diff --git a/src/Database/Reader.php b/src/Database/Reader.php index 794ecb97..93bb7e0d 100644 --- a/src/Database/Reader.php +++ b/src/Database/Reader.php @@ -70,6 +70,8 @@ public function __construct( public readonly array $locales = ['en'] ) { $this->dbReader = new DbReader($filename); + // The reader was just opened, so metadata() cannot report a closed reader. + // @phpstan-ignore missingType.checkedException $this->dbType = $this->dbReader->metadata()->databaseType; } @@ -356,6 +358,8 @@ public function metadata(): Metadata /** * Closes the GeoIP database and returns the resources to the system. + * + * @throws \BadMethodCallException if the database has already been closed */ public function close(): void { diff --git a/tests/GeoIp2/Test/Database/ReaderTest.php b/tests/GeoIp2/Test/Database/ReaderTest.php index fb327473..a9020b37 100644 --- a/tests/GeoIp2/Test/Database/ReaderTest.php +++ b/tests/GeoIp2/Test/Database/ReaderTest.php @@ -128,6 +128,16 @@ public function testClosedReader(): void $reader->city('81.2.69.160'); } + public function testCloseTwice(): void + { + $reader = new Reader('maxmind-db/test-data/GeoIP2-City-Test.mmdb'); + $reader->close(); + + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('closed'); + $reader->close(); + } + public function testAnonymousIp(): void { $reader = new Reader('maxmind-db/test-data/GeoIP2-Anonymous-IP-Test.mmdb'); From 6114a4b194454eeb6da7ce98900ab55cf39e95fa Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:00:44 -0700 Subject: [PATCH 07/12] Declare model class strings instead of suppressing reflection errors --- src/Database/Reader.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Database/Reader.php b/src/Database/Reader.php index 93bb7e0d..09159c1c 100644 --- a/src/Database/Reader.php +++ b/src/Database/Reader.php @@ -271,6 +271,8 @@ public function isp(string $ipAddress): Isp } /** + * @param class-string $class + * * @throws AddressNotFoundException * @throws InvalidDatabaseException * @throws \BadMethodCallException @@ -287,6 +289,8 @@ private function modelFor(string $class, string $type, string $ipAddress): objec } /** + * @param class-string $class + * * @throws AddressNotFoundException * @throws InvalidDatabaseException * @throws \BadMethodCallException @@ -303,6 +307,8 @@ private function flatModelFor(string $class, string $type, string $ipAddress): o } /** + * @param class-string $class + * * @throws AddressNotFoundException * @throws InvalidDatabaseException * @throws \BadMethodCallException @@ -313,8 +319,6 @@ private function flatModelFor(string $class, string $type, string $ipAddress): o private function getRecord(string $class, string $type, string $ipAddress): array { if (!str_contains($this->dbType, $type)) { - // Every caller passes the ::class constant of a model class. - // @phpstan-ignore missingType.checkedException $method = lcfirst((new \ReflectionClass($class))->getShortName()); throw new \BadMethodCallException( From fa55c96dc5529c99e36881846f2daab9702beec4 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:00:45 -0700 Subject: [PATCH 08/12] Document platform failures throughout database lookups --- src/Database/Reader.php | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/Database/Reader.php b/src/Database/Reader.php index 09159c1c..0cd5ddfe 100644 --- a/src/Database/Reader.php +++ b/src/Database/Reader.php @@ -61,6 +61,9 @@ class Reader implements ProviderInterface * @throws InvalidDatabaseException if the database is corrupt or invalid * @throws \InvalidArgumentException if the database file does not exist or * is not readable + * @throws \RuntimeException with the pure PHP reader, if metadata decoding + * needs gmp or bcmath and neither is installed, + * or a data offset exceeds the platform limit * @throws \UnexpectedValueException if the size of the database file * cannot be determined */ @@ -88,6 +91,9 @@ public function __construct( * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only + * @throws \RuntimeException with the pure PHP reader, if decoding needs + * gmp or bcmath and neither is installed, + * or a data offset exceeds the platform limit */ public function city(string $ipAddress): City { @@ -107,6 +113,9 @@ public function city(string $ipAddress): City * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only + * @throws \RuntimeException with the pure PHP reader, if decoding needs + * gmp or bcmath and neither is installed, + * or a data offset exceeds the platform limit */ public function country(string $ipAddress): Country { @@ -126,6 +135,9 @@ public function country(string $ipAddress): Country * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only + * @throws \RuntimeException with the pure PHP reader, if decoding needs + * gmp or bcmath and neither is installed, + * or a data offset exceeds the platform limit */ public function anonymousIp(string $ipAddress): AnonymousIp { @@ -149,6 +161,9 @@ public function anonymousIp(string $ipAddress): AnonymousIp * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only + * @throws \RuntimeException with the pure PHP reader, if decoding needs + * gmp or bcmath and neither is installed, + * or a data offset exceeds the platform limit */ public function anonymousPlus(string $ipAddress): AnonymousPlus { @@ -172,6 +187,9 @@ public function anonymousPlus(string $ipAddress): AnonymousPlus * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only + * @throws \RuntimeException with the pure PHP reader, if decoding needs + * gmp or bcmath and neither is installed, + * or a data offset exceeds the platform limit */ public function asn(string $ipAddress): Asn { @@ -195,6 +213,9 @@ public function asn(string $ipAddress): Asn * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only + * @throws \RuntimeException with the pure PHP reader, if decoding needs + * gmp or bcmath and neither is installed, + * or a data offset exceeds the platform limit */ public function connectionType(string $ipAddress): ConnectionType { @@ -218,6 +239,9 @@ public function connectionType(string $ipAddress): ConnectionType * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only + * @throws \RuntimeException with the pure PHP reader, if decoding needs + * gmp or bcmath and neither is installed, + * or a data offset exceeds the platform limit */ public function domain(string $ipAddress): Domain { @@ -241,6 +265,9 @@ public function domain(string $ipAddress): Domain * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only + * @throws \RuntimeException with the pure PHP reader, if decoding needs + * gmp or bcmath and neither is installed, + * or a data offset exceeds the platform limit */ public function enterprise(string $ipAddress): Enterprise { @@ -260,6 +287,9 @@ public function enterprise(string $ipAddress): Enterprise * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only + * @throws \RuntimeException with the pure PHP reader, if decoding needs + * gmp or bcmath and neither is installed, + * or a data offset exceeds the platform limit */ public function isp(string $ipAddress): Isp { @@ -277,6 +307,7 @@ public function isp(string $ipAddress): Isp * @throws InvalidDatabaseException * @throws \BadMethodCallException * @throws \InvalidArgumentException + * @throws \RuntimeException */ private function modelFor(string $class, string $type, string $ipAddress): object { @@ -295,6 +326,7 @@ private function modelFor(string $class, string $type, string $ipAddress): objec * @throws InvalidDatabaseException * @throws \BadMethodCallException * @throws \InvalidArgumentException + * @throws \RuntimeException */ private function flatModelFor(string $class, string $type, string $ipAddress): object { @@ -313,6 +345,7 @@ private function flatModelFor(string $class, string $type, string $ipAddress): o * @throws InvalidDatabaseException * @throws \BadMethodCallException * @throws \InvalidArgumentException + * @throws \RuntimeException * * @return array{0:array, 1:int} */ From 1c8ae429f73ad5081ddeb55161398b63f6418e98 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:00:47 -0700 Subject: [PATCH 09/12] Clarify which reader versions reject nested lookups --- src/Database/Reader.php | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/Database/Reader.php b/src/Database/Reader.php index 0cd5ddfe..98763b29 100644 --- a/src/Database/Reader.php +++ b/src/Database/Reader.php @@ -87,7 +87,8 @@ public function __construct( * @throws InvalidDatabaseException if the database is corrupt or invalid * @throws \BadMethodCallException if the database type does not support * this method, the reader is closed, or - * a lookup is in progress + * a lookup is in progress (pure PHP reader + * 1.14.0 and later) * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only @@ -109,7 +110,8 @@ public function city(string $ipAddress): City * @throws InvalidDatabaseException if the database is corrupt or invalid * @throws \BadMethodCallException if the database type does not support * this method, the reader is closed, or - * a lookup is in progress + * a lookup is in progress (pure PHP reader + * 1.14.0 and later) * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only @@ -131,7 +133,8 @@ public function country(string $ipAddress): Country * @throws InvalidDatabaseException if the database is corrupt or invalid * @throws \BadMethodCallException if the database type does not support * this method, the reader is closed, or - * a lookup is in progress + * a lookup is in progress (pure PHP reader + * 1.14.0 and later) * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only @@ -157,7 +160,8 @@ public function anonymousIp(string $ipAddress): AnonymousIp * @throws InvalidDatabaseException if the database is corrupt or invalid * @throws \BadMethodCallException if the database type does not support * this method, the reader is closed, or - * a lookup is in progress + * a lookup is in progress (pure PHP reader + * 1.14.0 and later) * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only @@ -183,7 +187,8 @@ public function anonymousPlus(string $ipAddress): AnonymousPlus * @throws InvalidDatabaseException if the database is corrupt or invalid * @throws \BadMethodCallException if the database type does not support * this method, the reader is closed, or - * a lookup is in progress + * a lookup is in progress (pure PHP reader + * 1.14.0 and later) * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only @@ -209,7 +214,8 @@ public function asn(string $ipAddress): Asn * @throws InvalidDatabaseException if the database is corrupt or invalid * @throws \BadMethodCallException if the database type does not support * this method, the reader is closed, or - * a lookup is in progress + * a lookup is in progress (pure PHP reader + * 1.14.0 and later) * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only @@ -235,7 +241,8 @@ public function connectionType(string $ipAddress): ConnectionType * @throws InvalidDatabaseException if the database is corrupt or invalid * @throws \BadMethodCallException if the database type does not support * this method, the reader is closed, or - * a lookup is in progress + * a lookup is in progress (pure PHP reader + * 1.14.0 and later) * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only @@ -261,7 +268,8 @@ public function domain(string $ipAddress): Domain * @throws InvalidDatabaseException if the database is corrupt or invalid * @throws \BadMethodCallException if the database type does not support * this method, the reader is closed, or - * a lookup is in progress + * a lookup is in progress (pure PHP reader + * 1.14.0 and later) * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only @@ -283,7 +291,8 @@ public function enterprise(string $ipAddress): Enterprise * @throws InvalidDatabaseException if the database is corrupt or invalid * @throws \BadMethodCallException if the database type does not support * this method, the reader is closed, or - * a lookup is in progress + * a lookup is in progress (pure PHP reader + * 1.14.0 and later) * @throws \InvalidArgumentException if the IP address is not valid, or if * it is an IPv6 address and the database * is IPv4-only From f91d0ce29ccdefe49cd4eeea5930550a8f6a31b3 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:00:48 -0700 Subject: [PATCH 10/12] Document HTTP setup failures across supported client versions --- src/WebService/Client.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/WebService/Client.php b/src/WebService/Client.php index 3150af24..2a85aa6c 100644 --- a/src/WebService/Client.php +++ b/src/WebService/Client.php @@ -78,6 +78,9 @@ class Client implements ProviderInterface * * `proxy` - The HTTP proxy to use. May include a schema, port, * username, and password, e.g., * `http://username:password@127.0.0.1:10`. + * + * @throws \RuntimeException if the cURL version or CA bundle cannot be set up + * @throws WebServiceException if HTTP client setup fails */ public function __construct( int $accountId, @@ -129,6 +132,8 @@ private function userAgent(): string * if a 200 status code is returned but the body is invalid. * @throws \InvalidArgumentException if something other than a single IP address or "me" is * passed to the method + * @throws \RuntimeException if the cURL version cannot be determined or + * the cURL handle cannot be initialized */ public function city(string $ipAddress = 'me'): City { @@ -159,6 +164,8 @@ public function city(string $ipAddress = 'me'): City * the body is invalid. * @throws \InvalidArgumentException if something other than a single IP address or "me" is * passed to the method + * @throws \RuntimeException if the cURL version cannot be determined or + * the cURL handle cannot be initialized */ public function country(string $ipAddress = 'me'): Country { @@ -190,6 +197,8 @@ public function country(string $ipAddress = 'me'): Country * if a 200 status code is returned but the body is invalid. * @throws \InvalidArgumentException if something other than a single IP address or "me" is * passed to the method + * @throws \RuntimeException if the cURL version cannot be determined or + * the cURL handle cannot be initialized */ public function insights(string $ipAddress = 'me'): Insights { @@ -212,6 +221,7 @@ public function insights(string $ipAddress = 'me'): Insights * @throws HttpException * @throws GeoIp2Exception * @throws \InvalidArgumentException + * @throws \RuntimeException * * @return TModel the corresponding model object, matching the passed class string */ From 3200d2ce6086e60403b5513307c53a066841b1a9 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:00:50 -0700 Subject: [PATCH 11/12] Declare exceptions on the provider interface --- src/ProviderInterface.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/ProviderInterface.php b/src/ProviderInterface.php index 8f40bee5..962ccaad 100644 --- a/src/ProviderInterface.php +++ b/src/ProviderInterface.php @@ -4,11 +4,19 @@ namespace GeoIp2; +use MaxMind\Db\Reader\InvalidDatabaseException; + interface ProviderInterface { /** * @param string $ipAddress an IPv4 or IPv6 address to lookup * + * @throws Exception\GeoIp2Exception if the address is not found or a web service error occurs + * @throws InvalidDatabaseException if the database is invalid + * @throws \BadMethodCallException if the database does not support the lookup or is closed + * @throws \InvalidArgumentException if the address is invalid or unsupported by the database + * @throws \RuntimeException if the database decoder or HTTP client cannot run + * * @return Model\Country a Country model for the requested IP address */ public function country(string $ipAddress): Model\Country; @@ -16,6 +24,12 @@ public function country(string $ipAddress): Model\Country; /** * @param string $ipAddress an IPv4 or IPv6 address to lookup * + * @throws Exception\GeoIp2Exception if the address is not found or a web service error occurs + * @throws InvalidDatabaseException if the database is invalid + * @throws \BadMethodCallException if the database does not support the lookup or is closed + * @throws \InvalidArgumentException if the address is invalid or unsupported by the database + * @throws \RuntimeException if the database decoder or HTTP client cannot run + * * @return Model\City a City model for the requested IP address */ public function city(string $ipAddress): Model\City; From 3ac207432ecee2832fc17811337d34a8a868d191 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Mon, 5 Oct 2026 19:50:10 -0700 Subject: [PATCH 12/12] Qualify HTTP setup exceptions by dependency version --- src/WebService/Client.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/WebService/Client.php b/src/WebService/Client.php index 2a85aa6c..90797d6d 100644 --- a/src/WebService/Client.php +++ b/src/WebService/Client.php @@ -79,8 +79,9 @@ class Client implements ProviderInterface * username, and password, e.g., * `http://username:password@127.0.0.1:10`. * - * @throws \RuntimeException if the cURL version or CA bundle cannot be set up - * @throws WebServiceException if HTTP client setup fails + * @throws \RuntimeException with web-service-common 0.11.x, if CA bundle setup fails, + * or with 0.11.1, if the cURL version cannot be determined + * @throws WebServiceException with web-service-common 0.12.0 and later, if HTTP client setup fails */ public function __construct( int $accountId, @@ -132,8 +133,8 @@ private function userAgent(): string * if a 200 status code is returned but the body is invalid. * @throws \InvalidArgumentException if something other than a single IP address or "me" is * passed to the method - * @throws \RuntimeException if the cURL version cannot be determined or - * the cURL handle cannot be initialized + * @throws \RuntimeException with web-service-common 0.11.1, if the cURL version + * cannot be determined or the cURL handle cannot be initialized */ public function city(string $ipAddress = 'me'): City { @@ -164,8 +165,8 @@ public function city(string $ipAddress = 'me'): City * the body is invalid. * @throws \InvalidArgumentException if something other than a single IP address or "me" is * passed to the method - * @throws \RuntimeException if the cURL version cannot be determined or - * the cURL handle cannot be initialized + * @throws \RuntimeException with web-service-common 0.11.1, if the cURL version + * cannot be determined or the cURL handle cannot be initialized */ public function country(string $ipAddress = 'me'): Country { @@ -197,8 +198,8 @@ public function country(string $ipAddress = 'me'): Country * if a 200 status code is returned but the body is invalid. * @throws \InvalidArgumentException if something other than a single IP address or "me" is * passed to the method - * @throws \RuntimeException if the cURL version cannot be determined or - * the cURL handle cannot be initialized + * @throws \RuntimeException with web-service-common 0.11.1, if the cURL version + * cannot be determined or the cURL handle cannot be initialized */ public function insights(string $ipAddress = 'me'): Insights {