Skip to content

Add IPGeolocation.io provider - #1276

Open
mateen993 wants to merge 1 commit into
geocoder-php:masterfrom
mateen993:add-ipgeolocation-provider
Open

mateen993 wants to merge 1 commit into
geocoder-php:masterfrom
mateen993:add-ipgeolocation-provider

Conversation

@mateen993

Copy link
Copy Markdown

Summary

This PR adds a new IP geocoding provider, Geocoder\Provider\IPGeolocation\IPGeolocation, for the IPGeolocation.io API. It will be published as geocoder-php/ipgeolocation-provider.

It is modeled on the existing Ipstack and IpInfo providers:

  • it extends AbstractHttpProvider and implements Provider;
  • it takes an API key in the constructor;
  • it supports IPv4 and IPv6 only.

No existing code changes. The only edits outside the new folder are list entries: the README table, the CI matrix, the subtree split and the phpunit placeholder.

Files changed

New provider folder, src/Provider/IPGeolocation/ (same layout as the other providers):

File Purpose
IPGeolocation.php The provider
Tests/IPGeolocationTest.php Unit tests (13), all with mocked HTTP responses
Tests/IntegrationTest.php ProviderIntegrationTest subclass (address and reverse disabled)
Tests/.cached_responses/ 2 recorded API responses, used by the integration test
composer.json Package geocoder-php/ipgeolocation-provider; requires php ^8.0, geocoder-php/common-http ^4.0, willdurand/geocoder ^4.0|^5.0
Readme.md, CHANGELOG.md, LICENSE Standard provider files (MIT license, copied from the other providers)
phpunit.xml.dist, .gitattributes, .gitignore, .github/workflows/provider.yml Copied from Ipstack with the env key renamed

Edits in the monorepo root:

File Change
README.md Row added to the "IP" providers table, in alphabetical order
.github/workflows/provider.yml IPGeolocation added to the provider matrix
.github/workflows/subtree.yml { folder: IPGeolocation, repository: ipgeolocation-provider } added
phpunit.xml.dist IPGEOLOCATION_API_KEY placeholder added

Usage

use Geocoder\Provider\IPGeolocation\IPGeolocation;
use Geocoder\Query\GeocodeQuery;

$provider = new IPGeolocation($httpClient, 'your-api-key');

$result = $provider->geocodeQuery(GeocodeQuery::create('8.8.8.8'));

A free API key is available at https://app.ipgeolocation.io/login.

How the provider calls the API

Each geocode query sends one GET request:

GET https://api.ipgeolocation.io/v3/ipgeo?apiKey={key}&ip={ip}&fields=location,time_zone.name[&lang={locale}]
  • fields: limits the response to the two objects the provider maps. It keeps the payload small and doesn't change the price: the base lookup costs 1 credit either way. It is available on every plan.
  • lang: sent only when the query has a locale (GeocodeQuery::withLocale()). English works on every plan; other languages need a paid plan, and on a free plan the API rejects them with HTTP 401.
  • The query string is built with http_build_query(), so IPv6 addresses are encoded correctly.
  • The provider doesn't use the API's optional paid modules (include=security, abuse and so on), so it works the same on free and paid keys.

Behaviour

Input or response Result
Empty API key in the constructor InvalidCredentials('No API key provided.')
Query text that isn't an IP (street address, hostname) UnsupportedOperation
127.0.0.1 or ::1 One localhost Address from getLocationForLocalhost(), no request sent (same as other IP providers)
reverseQuery() UnsupportedOperation
HTTP 200 with a location object One Address (see mapping below)
HTTP 200 without a location object Empty collection
HTTP 404: IP not in the IPGeolocation.io database Empty collection
HTTP 423: private or reserved (bogon) IP, e.g. 10.0.0.1 or 2001:db8::/32 Empty collection
HTTP 401 / 403 InvalidCredentials
HTTP 429 QuotaExceeded
Any other status ≥ 300 InvalidServerResponse
HTTP 200 with an empty or non-JSON body InvalidServerResponse

Why the provider checks the HTTP status itself. It sends the request through getHttpClient()->sendRequest() instead of getUrlContents(), the same approach MapQuest uses. getUrlContents() throws InvalidServerResponse for every status ≥ 300, but for this API 404 and 423 mean "no result for this IP", not "server error". Returning an empty collection matches what other providers do when an IP has no location. The 401/403, 429 and other status handling is identical to AbstractHttpProvider::getParsedResponse().

Field mapping

Address field API response field Example (83.227.123.8, from the recorded response)
coordinates latitude location.latitude (cast to float) 59.36888
coordinates longitude location.longitude (cast to float) 18.00843
locality location.city Solna
postalCode location.zipcode 169 03
adminLevels[1] name location.state_prov Stockholm County
adminLevels[1] code location.state_code SE-AB
adminLevels[2] name location.district Stockholm
country name location.country_name Sweden
country code location.country_code2 SE
timezone time_zone.name Europe/Stockholm
providedBy always ipgeolocation ipgeolocation

Notes on the mapping:

  • The admin level 1 code is passed through exactly as the API returns it, an ISO 3166-2 code with the country prefix (SE-AB, US-CA).
  • Empty strings become null. The API returns "" for fields that have no value, so the provider converts them. Admin levels are only added when they have a name.
  • streetName, streetNumber, subLocality and bounds are not set: the API has no matching field.

Tests

Unit tests (Tests/IPGeolocationTest.php, 13 tests, no network). They cover:

  • getName();
  • an empty API key;
  • a street address rejected;
  • localhost IPv4 and IPv6;
  • the exact request URL, including fields and lang;
  • a full IPv4 response mapped field by field;
  • empty-string fields becoming null;
  • 404 and 423 returning an empty collection;
  • 401 → InvalidCredentials;
  • 429 → QuotaExceeded;
  • reverseQuery() rejected.

The mocked response bodies are taken from the free-plan example in the IPGeolocation.io API reference.

Integration test (Tests/IntegrationTest.php). It sets $testAddress = false and $testReverse = false, like Ipstack. The two cached responses were recorded from the live API with a free-plan key, and neither contains the key:

  • 83.227.123.8: a full Swedish location (the mapping table above uses it).
  • 2001:0db8:0000:0042:0000:8a2e:0370:7334: the API's bogon response ("'2001:db8:0:42:0:8a2e:370:7334' is a bogon IP address."), which becomes an empty collection.

Results:

  • vendor/bin/phpunit src/Provider/IPGeolocation/Tests: 29 tests OK, 4 skipped (the address and reverse integration tests, which are disabled for IP providers).
  • vendor/bin/phpstan analyse src/Provider/IPGeolocation: no errors.
  • vendor/bin/php-cs-fixer fix --dry-run src/Provider/IPGeolocation: no changes.

Note for maintainers

After merge, the subtree split targets geocoder-php/ipgeolocation-provider, so that repository and its Packagist package need to be created before the first release. The provider's CHANGELOG.md starts at 1.0.0. I'm happy to change the package name or the version if you prefer something else.

Disclosure

I work at IPGeolocation.io. We will maintain this provider, keep it in step with the API, and respond to issues about it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants