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/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": { diff --git a/phpstan.neon b/phpstan.neon index 63aadbac..c861feba 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,3 +3,27 @@ parameters: paths: - src - tests + exceptions: + # These classes signal programmer errors, so callers need not declare them. + uncheckedExceptionClasses: + - Error + check: + missingCheckedExceptionInThrows: true + ignoreErrors: + # PHPUnit handles exceptions from test methods and their helpers. + - + 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 62bbf189..98763b29 100644 --- a/src/Database/Reader.php +++ b/src/Database/Reader.php @@ -51,12 +51,21 @@ 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 \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 */ public function __construct( string $filename, @@ -64,6 +73,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; } @@ -74,8 +85,16 @@ 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 (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 + * @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 { @@ -89,8 +108,16 @@ 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 (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 + * @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 { @@ -104,8 +131,16 @@ 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 (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 + * @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 { @@ -123,8 +158,16 @@ 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 (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 + * @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 { @@ -142,8 +185,16 @@ 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 (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 + * @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 { @@ -161,8 +212,16 @@ 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 (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 + * @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 { @@ -180,8 +239,16 @@ 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 (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 + * @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 { @@ -199,8 +266,16 @@ 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 (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 + * @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 { @@ -214,8 +289,16 @@ 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 (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 + * @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 { @@ -226,6 +309,15 @@ public function isp(string $ipAddress): Isp ); } + /** + * @param class-string $class + * + * @throws AddressNotFoundException + * @throws InvalidDatabaseException + * @throws \BadMethodCallException + * @throws \InvalidArgumentException + * @throws \RuntimeException + */ private function modelFor(string $class, string $type, string $ipAddress): object { [$record, $prefixLen] = $this->getRecord($class, $type, $ipAddress); @@ -236,6 +328,15 @@ private function modelFor(string $class, string $type, string $ipAddress): objec return new $class($record, $this->locales); } + /** + * @param class-string $class + * + * @throws AddressNotFoundException + * @throws InvalidDatabaseException + * @throws \BadMethodCallException + * @throws \InvalidArgumentException + * @throws \RuntimeException + */ private function flatModelFor(string $class, string $type, string $ipAddress): object { [$record, $prefixLen] = $this->getRecord($class, $type, $ipAddress); @@ -247,6 +348,14 @@ private function flatModelFor(string $class, string $type, string $ipAddress): o } /** + * @param class-string $class + * + * @throws AddressNotFoundException + * @throws InvalidDatabaseException + * @throws \BadMethodCallException + * @throws \InvalidArgumentException + * @throws \RuntimeException + * * @return array{0:array, 1:int} */ private function getRecord(string $class, string $type, string $ipAddress): array @@ -282,8 +391,9 @@ 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 */ @@ -294,6 +404,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/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; diff --git a/src/WebService/Client.php b/src/WebService/Client.php index 3150af24..90797d6d 100644 --- a/src/WebService/Client.php +++ b/src/WebService/Client.php @@ -78,6 +78,10 @@ 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 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, @@ -129,6 +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 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 { @@ -159,6 +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 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 { @@ -190,6 +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 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 { @@ -212,6 +222,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 */ diff --git a/tests/GeoIp2/Test/Database/ReaderTest.php b/tests/GeoIp2/Test/Database/ReaderTest.php index 562eee8c..a9020b37 100644 --- a/tests/GeoIp2/Test/Database/ReaderTest.php +++ b/tests/GeoIp2/Test/Database/ReaderTest.php @@ -118,6 +118,26 @@ 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 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');