timefrontiers/php-location

Get visitor's location information from IP address with multiple provider support

Maintainers

Package info

github.com/timefrontiers/php-location

pkg:composer/timefrontiers/php-location

Transparency log

Statistics

Installs: 78

Dependents: 0

Suggesters: 1

Stars: 0

Open Issues: 0

v1.1.0 2026-08-30 18:53 UTC

This package is auto-updated.

Last update: 2026-08-30 18:55:52 UTC


README

Explicit, privacy-aware IP geolocation for PHP 8.5 applications. Construction never sends a network request, forwarding headers are never trusted by default, and provider results are returned as complete immutable snapshots.

PHP Version License

Requirements

  • PHP 8.5+
  • ext-json
  • ext-curl only when using the supplied CurlHttpTransport
  • geoip2/geoip2 only when using MaxMindService::fromDatabase()
composer require timefrontiers/php-location

Explicit lookup

Inject a configured provider, then call locate() explicitly. Creating the provider or lookup object has no network side effect.

use TimeFrontiers\GeoIP\LocationLookup;
use TimeFrontiers\GeoIP\MaxMindService;

$lookup = new LocationLookup(
    MaxMindService::fromDatabase('/run/geoip/GeoLite2-City.mmdb'),
);

$location = $lookup->locate('8.8.8.8');
echo $location->country_code;

If no IP is supplied, the default resolver reads only REMOTE_ADDR. It ignores Forwarded, X-Forwarded-For, X-Real-IP, and every other forwarding header. A host behind a trusted proxy must inject a ClientIpResolverInterface whose implementation has already applied that host's trusted-proxy policy.

Public lookup uses explicit snapshots of the current IANA IPv4/IPv6 special- purpose and IPv6 allocated-global-unicast registries. It accepts a special range only when IANA marks global reachability affirmatively true; false, blank, conditional, and N/A ranges fail closed. Longest-prefix decisions retain the affirmative anycast allocations inside broader reserved blocks. IPv4- mapped/compatible, 6to4, TEREDO, private-embedded NAT64, unallocated, multicast, documentation, link-local, loopback, and reserved forms are rejected before calling the provider. A local database or application-owned resolver may deliberately inject IpAddressPolicy::allowNonPublic(); public remote services such as IpApiService still enforce public addresses at their own boundary.

The embedded registry snapshot is dated 2025-10-09/10. Review it against the IANA IPv4 special-purpose registry, IANA IPv6 special-purpose registry, and IANA IPv6 global-unicast registry before each release. Registry drift must fail closed and receive corpus tests.

Bounded HTTPS JSON provider

IpApiService is an adapter for the ip-api JSON response shape or a compatible application gateway. It requires an injected transport and an explicit HTTPS endpoint.

use TimeFrontiers\GeoIP\IpApiService;
use TimeFrontiers\GeoIP\LocationLookup;
use TimeFrontiers\Transport\CurlHttpTransport;
use TimeFrontiers\Transport\HttpRequestOptions;

$transport = new CurlHttpTransport(['geo.example.com']);
$provider = new IpApiService(
    transport: $transport,
    endpoint: 'https://geo.example.com/ip-api-json',
    authorization: 'Bearer ' . $_ENV['GEO_GATEWAY_TOKEN'],
    options: new HttpRequestOptions(
        connectTimeoutMilliseconds: 750,
        totalTimeoutMilliseconds: 2500,
        maximumResponseBytes: 32768,
    ),
);

$location = (new LocationLookup($provider))->locate('1.1.1.1');

The supplied cURL transport permits HTTPS only, verifies TLS, disables redirects, requires an exact host allowlist, rejects private/reserved DNS answers, pins the approved DNS result for the request, bounds connect/total timeouts, and aborts oversized responses. It explicitly disables HTTPS_PROXY, ALL_PROXY, and every other ambient libcurl proxy through handle options, so validated DNS pinning always describes the direct path. Proxying is not a feature of this transport; an application that needs it must supply a separate transport with an explicit proxy allowlist, TLS, authentication, resolution, and redaction policy. IpApiService additionally requires HTTP 200 JSON, validates every consumed field and coordinate, and verifies that the response describes the requested IP.

The public ip-api free endpoint is intentionally unsupported because it is HTTP-only and disallows commercial use. Direct ip-api pro authentication is also not built in because its key is a GET parameter; v1.1 never puts secrets in URLs. Use an application-owned HTTPS gateway that accepts an authorization header, or inject another GeoIPInterface implementation. Authorization is redacted from debug state and provider objects cannot be serialized.

Host enrichment

This package contains no Linktude globals, SQL, schemas, or database dependency. Applications that need their own city/state codes may inject a LocationDataEnricherInterface. An enricher receives a complete immutable snapshot and must return another one, for example with withHostCodes(). Database implementations remain in the host and must use prepared, deterministic queries, distinguish database failure from no match, and escape wildcards if fuzzy matching is deliberately retained.

MaxMind

MaxMindService accepts the narrow MaxMindReaderInterface; use fromDatabase() when geoip2/geoip2 is installed. Capability, path readability, record shape, and coordinate ranges are validated. Raw MaxMind exceptions and database paths are not copied into public errors.

Legacy Location adapter

TimeFrontiers\Location and refresh() remain deprecated migration adapters. Construction no longer performs a lookup. Every public property starts with a safe value, and refresh() assigns a complete validated snapshot only after lookup and enrichment succeed. Failure preserves the previous snapshot and records only a safe message with blank file/line fields.

$legacy = new \TimeFrontiers\Location('8.8.8.8', $provider);
$legacy->refresh();
echo $legacy->country_code;

Prefer LocationLookup::locate() and immutable LocationData in new code.

Currency symbols

CurrencySymbols::get() normalizes a code to uppercase and returns a display-only symbol hint. Unknown codes return the normalized code. It is not an ISO currency authority and must not decide settlement currency or money rules.

Privacy and retention

An IP address is personal data in many jurisdictions. Before enabling remote lookup, the application owner must document the lawful basis or consent, specific purpose, provider/subprocessor, disclosed fields, geographic precision, cache lifetime, retention period, deletion process, and user notice.

The package provides no cache or persistence by default. Minimize precision and retention, do not log requested IPs, provider bodies, authorization, or exact coordinates unless the documented purpose requires them, and never reuse location collected for one purpose in an unrelated decision.

Quality gates

composer validate --strict
composer audit
composer check

License

MIT License