gam6itko / ipquery-client
Framework-agnostic PHP client for a self-hosted IPQuery geo service (akyriako/ipquery)
Requires
- php: >=8.1
- ext-json: *
- psr/http-client: ^1.0.3
- psr/http-factory: ^1.1
- psr/http-message: ^1.1 || ^2.0
- psr/simple-cache: ^1.0 || ^2.0 || ^3.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- infection/infection: ^0.35 || ^0.27
- nyholm/psr7: ^1.8
- phpunit/phpunit: ^13.0 || ^12.0 || ^11.0 || ^10.0
- vimeo/psalm: ^6.0
Suggests
- guzzlehttp/guzzle: PSR-18 HTTP client with PSR-7/17 implementation
- nyholm/psr7: PSR-7/17 implementation (request, URI and response factories)
- psr/simple-cache-implementation: PSR-16 cache for CachedIPQuery
- symfony/http-client: PSR-18 HTTP client
This package is auto-updated.
Last update: 2026-08-31 12:20:23 UTC
README
Framework-agnostic PHP client for a self-hosted IPQuery geo service: resolve country, ISP and risk data by IP address. Built on PSR-18 (HTTP client) and PSR-17 (HTTP factories), so it works with any PSR-7 implementation.
Important
This client targets the self-hosted akyriako/ipquery
server (endpoint GET /lookup/{ip}). It is NOT compatible with the public SaaS
ipquery.io — that service has a different API (endpoints and response
shape). For ipquery.io use one of these instead:
Installation
composer require gam6itko/ipquery-client
# plus any PSR-18 client and PSR-7/17 implementation, e.g.:
composer require symfony/http-client nyholm/psr7
Usage
use Gam6itko\IPQuery\IPQueryClient; use Gam6itko\IPQuery\IPQueryRequestFactory; use Nyholm\Psr7\Factory\Psr17Factory; use Symfony\Component\HttpClient\Psr18Client; $psr17 = new Psr17Factory(); $client = new IPQueryClient( httpClient: new Psr18Client(), // IPQueryRequestFactory prepends the geo-service base URI to the relative path. requestFactory: new IPQueryRequestFactory($psr17, $psr17, 'http://localhost:8080'), ); $result = $client->lookup('8.8.8.8'); // IPv4 or IPv6; throws Gam6itko\IPQuery\LookupException on failure echo $result['location']['country_code']; // ISO 3166-1 alpha-2 in UPPER case, e.g. "US" // Caller's own IP as seen by the geo service: $client->own(); // full metadata (GET /own/all), same shape as lookup() $client->ownIp(); // just the IP string (GET /own)
Endpoints
| Method | Server endpoint | Returns |
|---|---|---|
lookup(string $ip) |
GET /lookup/{ip} |
TIPQueryResult for the given IP |
own() |
GET /own/all |
TIPQueryResult for the caller's own IP |
ownIp() |
GET /own |
non-empty-string — the caller's own IP |
own()/ownIp()live onIPQueryClientonly;LookupInterface(the cacheable, mockable contract used viaCachedIPQuery/IPQueryStub) exposes justlookup().
Error handling
Every failure throws an exception implementing Gam6itko\IPQuery\ExceptionInterface:
ExceptionInterface (\Throwable)
└── LookupException transport error, non-200 status, invalid JSON, unexpected payload
└── InvalidIpException $ip is not a valid IP address (thrown before any HTTP request)
lookup() accepts both IPv4 and IPv6 — a string that is not a valid IP address is rejected up
front with InvalidIpException (a LookupException, so existing catch blocks keep working). For an
unexpected HTTP status the code is available via getStatusCode() (null for transport/parsing/
validation failures):
try { $client->lookup('8.8.8.8'); } catch (\Gam6itko\IPQuery\LookupException $e) { $status = $e->getStatusCode(); // e.g. 503, or null }
Caching
CachedIPQuery decorates any IPQueryInterface with a PSR-16 cache (the IP-to-country
mapping changes rarely; failures are never cached):
use Gam6itko\IPQuery\CachedIPQuery; $cached = new CachedIPQuery($client, $psr16Cache); $cached->lookup('8.8.8.8');
Retrying transient failures
RetryingIPQuery decorates any LookupInterface and retries transient failures with
exponential backoff — a transport error, a 5xx status, or 429. Deterministic failures
(InvalidIpException, other 4xx) fail fast and are never retried:
use Gam6itko\IPQuery\RetryingIPQuery; $resilient = new RetryingIPQuery($client, maxAttempts: 3, baseDelayMs: 100); // Compose with the cache — retry the live call, then cache the successful result: $cached = new CachedIPQuery(new RetryingIPQuery($client), $psr16Cache);
Override which failures are retried via the $retryable callback
(\Closure(LookupException $e, int $attempt): bool).
Testing
Use IPQueryStub as a test double — it returns a canned result or throws a given exception:
use Gam6itko\IPQuery\IPQueryStub; use Gam6itko\IPQuery\LookupException; new IPQueryStub($lookupResult); new IPQueryStub(new LookupException('geo is down'));
Response contract
lookup() returns the decoded /lookup/{ip} payload (psalm type TIPQueryResult):
| Key | Shape |
|---|---|
ip |
string |
isp |
{asn, org, isp} |
location |
{country, country_code, city, state, zipcode, latitude, longitude, timezone, localtime} |
risk |
{abuse_confidence_score, usage_type, is_tor, total_reports, number_of_users_reported, last_reported_at} |
Development
composer tests # phpunit composer psalm # static analysis (strict, full type coverage) composer csfix # php-cs-fixer
License
MIT — see LICENSE.