Custom providers
Register a closure provider at runtime — for example in a service provider’s boot() — then add its name to the pipeline:
use RoundlyConsulting\Geolocation\Facades\Geolocation;
// e.g. in a service provider's boot()
Geolocation::extend('my_provider', fn () => new MyProvider());// config/geolocation.php
'pipeline' => ['maxmind_database', 'my_provider', 'ipinfo', 'default'],Implementing a location provider
Implement GeolocationProvider. Return a Location, or null to defer to the next provider — an empty Location ($location->isEmpty()) defers too. An exception your provider throws aborts the lookup:
use RoundlyConsulting\Geolocation\DataTransferObjects\GeolocationQuery;
use RoundlyConsulting\Geolocation\DataTransferObjects\Location;
use RoundlyConsulting\Geolocation\Enum\GeolocationType;
use RoundlyConsulting\Geolocation\GeolocationProvider;
final class OfficeNetworkProvider implements GeolocationProvider
{
public function locate(GeolocationQuery $query): ?Location
{
if ($query->ipAddress === null || ! str_starts_with($query->ipAddress, '10.20.')) {
return null; // not ours — defer to the next provider
}
return new Location(
humanReadable: 'Example HQ, Springfield',
street: 'Main Street 1',
city: 'Springfield',
countryIsoCode: 'US',
latitude: 39.7817,
longitude: -89.6501,
type: GeolocationType::Ip,
);
}
}Instead of extend(), you can add the class to the providers map. Providers are resolved from the container, so constructor dependencies are injected:
// config/geolocation.php — resolved from the container, so constructor dependencies are injected
'providers' => [
// ...the bundled providers
'office' => \App\Geolocation\OfficeNetworkProvider::class,
],
'pipeline' => ['office', 'maxmind_database', 'ipinfo', 'default'],Implementing a distance provider
Implement DistanceProvider to answer distance() calls. A class can implement both contracts, as the bundled Google provider does:
use RoundlyConsulting\Geolocation\DataTransferObjects\Distance;
use RoundlyConsulting\Geolocation\DataTransferObjects\DistanceQuery;
use RoundlyConsulting\Geolocation\DistanceProvider;
final class StraightLineProvider implements DistanceProvider
{
public function distance(DistanceQuery $query): ?Distance
{
$metres = (int) round($query->from()->distanceTo($query->to()));
return new Distance(
humanReadableDistance: round($metres / 1000, 1).' km',
distanceInMeters: $metres,
humanReadableDuration: '',
durationInSeconds: 0,
type: $query->type,
);
}
}Reading call-time overrides
Use the HasProviderOverrides trait to read call-time values. $this->override('key') returns the withTimeout() value or a value aimed at your provider’s own name with withToken() / withConfig(), and null when the key isn’t set for this call:
use RoundlyConsulting\Geolocation\Concerns\HasProviderOverrides;
final class MyProvider implements GeolocationProvider
{
use HasProviderOverrides;
public function locate(GeolocationQuery $query): ?Location
{
$token = $this->override('token') ?? config('services.my_geo.token');
$timeout = $this->override('timeout') ?? 5;
$region = $this->override('region'); // any key passed through withConfig()
// ...
}
}
// The provider sees withTimeout() plus the values aimed at its own name:
Geolocation::withConfig('my_provider', ['region' => 'eu'])->locateIp($ip);Rate-limiting a custom provider
Send your HTTP calls through the InteractsWithRateLimits trait to pace them like the bundled providers. throttled() reads geolocation.services.{name}.rate_limits and keys the budget geolocation:{name}:{owner}:
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use RoundlyConsulting\Geolocation\Concerns\InteractsWithRateLimits;
final class NominatimProvider implements GeolocationProvider
{
use InteractsWithRateLimits;
public function locate(GeolocationQuery $query): ?Location
{
if ($query->address === null) {
return null;
}
// Paced by geolocation.services.nominatim.rate_limits, keyed geolocation:nominatim:{owner}
$response = $this->throttled('nominatim', fn (): Response => Http::get(
'https://nominatim.example.com/search',
['q' => $query->address],
));
// ...map $response->json() onto a Location, or return null
}
}Then give the provider a rate_limits block under services. OpenStreetMap’s Nominatim, for example, enforces a hard 1 request per second:
// config/geolocation.php → 'services'
'nominatim' => [
// ...your provider settings...
'rate_limits' => [
'enabled' => true,
'limit' => 1,
'per' => 'second',
'adaptive' => true,
],
],Google Places for Laravel plugs in the same way: when both packages are installed it calls extend() to register a google_places provider you can add to the pipeline.
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.