gam6itko/ipquery-client

Framework-agnostic PHP client for a self-hosted IPQuery geo service (akyriako/ipquery)

Maintainers

Package info

github.com/gam6itko/php-ipquery-client

pkg:composer/gam6itko/ipquery-client

Transparency log

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.1.0 2026-08-31 12:11 UTC

This package is auto-updated.

Last update: 2026-08-31 12:20:23 UTC


README

CI Latest Stable Version License

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 on IPQueryClient only; LookupInterface (the cacheable, mockable contract used via CachedIPQuery / IPQueryStub) exposes just lookup().

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.