timefrontiers / php-location
Get visitor's location information from IP address with multiple provider support
Requires
- php: >=8.5
- ext-json: *
Requires (Dev)
- php-parallel-lint/php-parallel-lint: ^1.4
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^10.5
Suggests
- ext-curl: Required only when using the supplied bounded CurlHttpTransport
- geoip2/geoip2: Required only when using MaxMindService::fromDatabase()
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.
Requirements
- PHP 8.5+
- ext-json
- ext-curl only when using the supplied
CurlHttpTransport geoip2/geoip2only when usingMaxMindService::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