A modern, PSR-based PHP client for the ipstack API.
- PSR-18 transport architecture
- Factory-based client construction
- Typed result models (
IpstackResult,Country,Region, etc.) - Bulk lookup support (up to 50 IPs per request)
- Domain exceptions mapped from ipstack API error codes
- PHP
8.3to8.5(>=8.3 <8.6) psr/http-clientpsr/http-factorypsr/http-message- A concrete PSR-18 client + PSR-17 factory implementation (for example
symfony/http-client+nyholm/psr7)
composer require sudiptpa/ipstackOptional (for the PSR-18 quick-start adapter stack shown below):
composer require symfony/http-client nyholm/psr7Core layers:
Ipstack::factory()configures and builds the clientIpstackClientruns lookup operationsTransportInterfaceabstracts HTTP transport (Psr18Transportincluded)IpstackResultMappermaps API payloads to typed models
Main entry points:
Ipstack\\IpstackIpstack\\Factory\\IpstackFactoryIpstack\\Client\\IpstackClientIpstack\\Client\\Options
<?php
declare(strict_types=1);
use Ipstack\Ipstack;
use Nyholm\Psr7\Factory\Psr17Factory;
use Symfony\Component\HttpClient\Psr18Client;
$client = Ipstack::factory()
->withAccessKey('YOUR_ACCESS_KEY')
->withPsr18(new Psr18Client(), new Psr17Factory())
->build();
$result = $client->lookup('8.8.8.8');
echo $result->formatted();use Ipstack\Client\IpstackClient;
use Ipstack\Ipstack;
use Nyholm\Psr7\Factory\Psr17Factory;
use Symfony\Component\HttpClient\Psr18Client;
function ipstackClient(string $accessKey): IpstackClient
{
return Ipstack::factory()
->withAccessKey($accessKey)
->withPsr18(new Psr18Client(), new Psr17Factory())
->build();
}$client = Ipstack::factory()
->withAccessKey('YOUR_ACCESS_KEY')
->withBaseUrl('https://api.ipstack.com')
->withPsr18(new Psr18Client(), new Psr17Factory())
->build();use Ipstack\Client\Options;
$options = Options::create()
->fields(['ip', 'country_name', 'region_name', 'city', 'zip'])
->language('en')
->security(true)
->hostname(true);
$result = $client->lookup('8.8.8.8', $options);
echo $result->ip . PHP_EOL;
echo $result->formatted() . PHP_EOL;
echo ($result->country->name ?? 'unknown') . PHP_EOL;$result = $client->lookupRequester();
echo $result->ip . PHP_EOL;$results = $client->lookupBulk(['8.8.8.8', '1.1.1.1']);
echo 'count=' . count($results) . PHP_EOL;
foreach ($results as $row) {
echo $row->ip . ' => ' . $row->formatted() . PHP_EOL;
}Notes:
- Empty list returns an empty
Ipstack\\Model\\IpstackCollection - More than 50 IPs throws
Ipstack\\Exception\\IpstackException
$result = $client->lookup('8.8.8.8');
$raw = $result->raw();
$json = json_encode($result, JSON_PRETTY_PRINT);
echo $json . PHP_EOL;use Ipstack\Ipstack;
use Ipstack\Transport\TransportInterface;
final class DemoTransport implements TransportInterface
{
public function get(string $url, array $query): array
{
return [
'ip' => '8.8.8.8',
'country_name' => 'United States',
'country_code' => 'US',
'region_name' => 'California',
'region_code' => 'CA',
'city' => 'Mountain View',
'zip' => '94035',
];
}
}
$client = Ipstack::factory()
->withAccessKey('DUMMY_KEY')
->withTransport(new DemoTransport())
->build();
echo $client->lookup('8.8.8.8')->formatted();use Ipstack\Exception\ApiErrorException;
use Ipstack\Exception\BatchNotSupportedException;
use Ipstack\Exception\InvalidFieldsException;
use Ipstack\Exception\InvalidResponseException;
use Ipstack\Exception\IpstackException;
use Ipstack\Exception\RateLimitException;
use Ipstack\Exception\TooManyIpsException;
use Ipstack\Exception\TransportException;
try {
$result = $client->lookup('8.8.8.8');
} catch (RateLimitException $e) {
// 104
} catch (InvalidFieldsException $e) {
// 301
} catch (TooManyIpsException $e) {
// 302
} catch (BatchNotSupportedException $e) {
// 303
} catch (TransportException | InvalidResponseException $e) {
// network/HTTP/JSON transport failure
} catch (ApiErrorException $e) {
// other API-side errors
} catch (IpstackException $e) {
// any other library-level exception
}lookup*() returns Ipstack\\Model\\IpstackResult with nested typed objects:
country(Country)region(Region)location(Location|null)connection(Connection|null)security(Security|null)routing(Routing|null)
Helpers:
$result->formatted()returns a readable location string$result->raw()returns original decoded payloadjson_encode($result)serializes raw payload (JsonSerializable)
All package exceptions extend Ipstack\\Exception\\IpstackException.
Transport/response exceptions:
TransportExceptionInvalidResponseException
API exceptions:
ApiErrorException(base)RateLimitExceptionfor code104InvalidFieldsExceptionfor code301TooManyIpsExceptionfor code302BatchNotSupportedExceptionfor code303
Use an environment variable and run this one-liner:
IPSTACK_ACCESS_KEY=your_key php -r 'require "vendor/autoload.php"; $c=Ipstack\Ipstack::factory()->withAccessKey(getenv("IPSTACK_ACCESS_KEY"))->withPsr18(new Symfony\Component\HttpClient\Psr18Client(), new Nyholm\Psr7\Factory\Psr17Factory())->build(); echo $c->lookup("8.8.8.8")->formatted(), PHP_EOL;'InvalidArgumentException: Access key is required.- Call
->withAccessKey('...')before->build().
- Call
InvalidArgumentException: No transport configured...- Configure either
->withPsr18(...)or->withTransport(...).
- Configure either
TransportException: Unexpected HTTP status ...- Check network access, endpoint, and ipstack key validity.
InvalidResponseException: Invalid JSON response- Usually an upstream/proxy response issue; inspect raw HTTP response.
RateLimitException(104)- Monthly/request quota reached on your ipstack account.
Some ipstack fields/features may depend on plan tier (for example security, hostname, and parts of connection/routing data). If a field is unavailable on your plan, ipstack may omit it or return an API error.
Compared to the legacy API style:
Sujip\\Ipstack\\Ipstackis replaced byIpstack\\Ipstack- Constructor-style usage is replaced by factory-style configuration
- Legacy root convenience accessors are replaced by typed result objects from lookup methods
secure()is no longer the documented toggle pattern- Transport wiring is explicit and PSR-based
- Bulk lookup is now
lookupBulk(array $ips)with a strict max of 50 IPs/request
Use this quick mapping to migrate legacy usage to the current API.
| v1 (legacy) | v2 (current) |
|---|---|
new Sujip\\Ipstack\\Ipstack($ip) |
Build once, then call lookup: Ipstack::factory()->withAccessKey(...)->withPsr18(...)->build()->lookup($ip) |
new Sujip\\Ipstack\\Ipstack($ip, $apiKey) |
Ipstack::factory()->withAccessKey($apiKey)->withPsr18(...)->build() |
$ipstack->country() |
$client->lookup($ip)->country->name |
$ipstack->region() |
$client->lookup($ip)->region->name |
$ipstack->city() |
$client->lookup($ip)->city |
$ipstack->formatted() |
$client->lookup($ip)->formatted() |
$ipstack->secure() |
Use configured base URL + transport: withBaseUrl(...) + withPsr18(...) |
| N/A (or ad-hoc loops) | Native bulk call: $client->lookupBulk([$ip1, $ip2]) |
| Generic runtime/API failures | Typed exception model (RateLimitException, InvalidFieldsException, TransportException, etc.) |
- Replace direct constructor usage with factory-based client construction.
- Create one reusable
IpstackClientservice and inject it where needed. - Replace legacy convenience getters with
lookup()result model access. - Add typed exception handling around lookup calls.
- Add a smoke test (local fake transport + real API env test) before release.
CI runs the following checks across PHP 8.3, 8.4, and 8.5:
composer testcomposer stan(onprefer-stablematrix jobs)
composer test
composer stan
composer rector
composer rector:checkSee CHANGELOG.md for release notes (latest hardening release: v2.1.0).
- Sujip Thapa (
sudiptpa@gmail.com)
MIT. See LICENSE.