NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

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

NameClassLocationDistanceSource
maxmind_databaseMaxMindDatabaseProviderby IPnoLocal .mmdb file (native reader)
maxmind_webMaxMindWebServiceProviderby IPnoMaxMind GeoIP2 Precision web service
ip2locationIP2LocationProviderby IPnoip2location.io HTTP API
ipinfoIpInfoProviderby IPnoipinfo.io HTTP API
googleGoogleProviderby coordinates or addressyesGoogle Geocoding + Distance Matrix
defaultDefaultLocationProviderstatic fallback, once configurednoConfig 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 crypto

By 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.