How it works
Every lookup walks an ordered pipeline of named providers and returns the first non-null answer. A GeolocationProvider turns a GeolocationQuery (IP, coordinates or address) into a Location; a DistanceProvider turns a DistanceQuery (two coordinate pairs plus a travel mode) into a Distance. Providers that don’t implement the contract a call needs are skipped:
use RoundlyConsulting\Geolocation\DataTransferObjects\Distance;
use RoundlyConsulting\Geolocation\DataTransferObjects\DistanceQuery;
use RoundlyConsulting\Geolocation\DataTransferObjects\GeolocationQuery;
use RoundlyConsulting\Geolocation\DataTransferObjects\Location;
// RoundlyConsulting\Geolocation\GeolocationProvider
public function locate(GeolocationQuery $query): ?Location;
// RoundlyConsulting\Geolocation\DistanceProvider
public function distance(DistanceQuery $query): ?Distance;Bundled providers
| Name | Class | Location | Distance | Source |
|---|---|---|---|---|
maxmind_database | MaxMindDatabaseProvider | by IP | no | Local .mmdb file (native reader) |
maxmind_web | MaxMindWebServiceProvider | by IP | no | MaxMind GeoIP2 Precision web service |
ip2location | IP2LocationProvider | by IP | no | ip2location.io HTTP API |
ipinfo | IpInfoProvider | by IP | no | ipinfo.io HTTP API |
google | GoogleProvider | by coordinates or address | yes | Google Geocoding + Distance Matrix |
default | DefaultLocationProvider | static fallback, once configured | no | Config values |
Both MaxMind providers are disabled by default and return null immediately until you enable them and supply credentials or a database path.
The pipeline
Providers are consulted in pipeline order; reorder them in config to change precedence:
// config/geolocation.php — consulted top to bottom, the first non-null answer wins
'pipeline' => ['maxmind_database', 'maxmind_web', 'ip2location', 'ipinfo', 'google', 'default'],Pipeline names that are neither in the providers map nor registered with extend() are skipped silently. A provider that can’t serve a query — for example Google for an IP, or an IP provider for an address — returns null and the next one is tried. Error responses degrade to null too, so a failing provider hands over to the next one.
A provider whose API can’t be reached at all — a timeout, a DNS failure, a refused connection — counts as a miss as well: the pipeline moves on instead of throwing, and the error is reported, with credentials redacted, on the LocationResolutionFailed event. A few errors still abort the lookup on purpose: a fail-fast RateLimitExceededException, a missing or corrupt MaxMind database, and any exception your own provider throws (see Exceptions).
Empty answers
An empty answer counts as no answer. A Location with no address part, no country and 0,0 coordinates ($location->isEmpty()) — such as IP2Location’s reply for a private IP — is skipped and the next provider is asked. A Location you get back therefore always places something, though a coarse IP match can still lack a city or a country.
What calls the network by default
With the shipped config an IP lookup sends the IP to api.ip2location.io and then ipinfo.io, even without IP2LOCATION_API_KEY or IPINFO_TOKEN (the request is then unauthenticated). An address or coordinate lookup, and every distance, goes to Google, which needs GOOGLE_MAPS_API_KEY to succeed. maxmind_database and default never touch the network, and in tests Geolocation::fake() never calls a provider.
Trim the pipeline to the providers you have credentials for — every provider left in it is consulted in turn until one answers, and each HTTP provider is a real outbound request:
// Only the providers you have credentials for: offline first, fallback last
'pipeline' => ['maxmind_database', 'ipinfo', 'google', 'default'],To keep visitor IPs on your own servers, leave only the offline providers:
// Keep visitor IPs on your own servers — no provider here calls the network
'pipeline' => ['maxmind_database', 'default'],The default fallback
The default provider answers only once you configure a default location (any default.* value). With the shipped empty values it answers null as well, so an unresolved lookup stays null. When it does answer, the Location has type GeolocationType::Default, so you can tell a fallback from a real lookup:
use RoundlyConsulting\Geolocation\Enum\GeolocationType;
use RoundlyConsulting\Geolocation\Facades\Geolocation;
$location = Geolocation::locateRequest();
if ($location?->type === GeolocationType::Default) {
// No real provider answered — this is the configured fallback location.
}The fallback is never cached — it only answered because the real providers did not, so the next call asks them again. Leave default.* empty, or remove default from the pipeline, if you would rather receive null (and a LocationResolutionFailed event) when nothing resolves.
Named providers
The providers key is a named map — the pipeline, provider() and using() all refer to providers by name. When none of the pipeline names match the map, the map’s own order is used. A flat list of class-strings is no longer accepted:
use RoundlyConsulting\Geolocation\Providers\DefaultLocationProvider;
use RoundlyConsulting\Geolocation\Providers\IpInfoProvider;
// Every entry needs a name — the pipeline and provider() address providers by it
'providers' => [
'ipinfo' => IpInfoProvider::class,
'default' => DefaultLocationProvider::class,
],
// An unnamed entry throws UnknownProviderException on the first lookup
'providers' => [IpInfoProvider::class],Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.