pulkitjalan/geoip
Laravel package for IP geolocation using MaxMind GeoIP2 databases. Resolve country, region, city, postal code, coordinates, timezone, and currency for any IP address through a simple facade, with configurable database paths and caching support. Ideal for localization, analytics, and fraud checks.
Install the package:
composer require pulkitjalan/geoip
Laravel auto-discovers PulkitJalan\GeoIP\GeoIPServiceProvider. If you're on an older Laravel or have discovery disabled, register the provider and add the GeoIP facade alias in config/app.php.
Publish the config:
php artisan vendor:publish --provider="PulkitJalan\GeoIP\GeoIPServiceProvider"
This gives you config/geoip.php with a driver key and a services array of available providers.
Pick a driver and set the matching env vars. The default (free, keyless) service works out of the box for a smoke test; paid/premium drivers (MaxMind, ipinfo, ipdata, etc.) need an API token or a local .mmdb path. Keep credentials in .env and reference them from the config file.
First use case — resolve the visitor's country inside a controller or middleware:
use PulkitJalan\GeoIP\Facades\GeoIP;
$location = GeoIP::getLocation($request->ip());
$location->countryCode; // "US"
$location->country; // "United States"
$location->city;
$location->lat;
$location->toArray();
Calling GeoIP::getLocation() with no argument falls back to the current request's IP.
Where to look first: config/geoip.php for the default driver and service list, and the Location value object (returned by getLocation()) for the fields each driver can populate. Start by writing one test that geolocates a known IP through your chosen driver — it tells you immediately whether credentials and network access are wired correctly.
Middleware for locale/currency. Resolve the location once per request, stash it on the request or a singleton, and derive locale, currency, and timezone for the rest of the request lifecycle:
class SetLocaleFromIp
{
public function handle($request, Closure $next)
{
try {
$location = GeoIP::getLocation($request->ip());
app()->setLocale(config("locales.{$location->countryCode}", 'en'));
} catch (\Throwable $e) {
report($e); // never break the request over geo lookup
}
return $next($request);
}
}
Wrap it behind your own service. Bind a GeoLocator interface in the container and inject GeoIP into a single implementation. This keeps the rest of the codebase independent of the package's API and makes it trivial to stub in tests or swap providers later.
Cache by IP. External geolocation APIs are rate-limited and latency-prone. Cache the normalized result keyed on the IP for 24h–30d (residential IPs rarely move; mobile IPs are noisier):
public function locate(?string $ip): ?LocationData
{
if (!$ip || !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return null;
}
return Cache::remember("geo:{$ip}", now()->addDay(), fn () => $this->map(GeoIP::getLocation($ip)));
}
Fallback chains. Configure two services (one primary, one cheap backup). Catch the driver exception and retry with GeoIP::setDriver('...') or re-resolve a second instance from the container with alternative config.
Asynchronous enrichment. Don't block registration/login on a network call. Dispatch a queued EnrichUserGeo job after the user record exists; it fills country_code, timezone, and currency on the user model.
Normalize to a DTO. Map the package Location into your own array/DTO immediately: ['country' => $loc->countryCode, 'city' => $loc->city, 'lat' => $loc->lat, ...]. Different drivers return different field sets, so centralize null handling in one place.
Testing. Never hit real services in CI. Fake the underlying HTTP client (Http::fake()) for HTTP-based drivers, or bind a fake GeoLocator implementation in the container. The Location object is easy to construct/mock for unit-testing presentation and mapping logic.
127.0.0.1, 10.0.0.0/8, 192.168.0.0/16, and IPv6 link-local addresses will return null or throw depending on the driver. Always guard before calling, and have a sensible default for local development.request()->ip() returns the proxy's IP unless you have TrustProxies configured. Geoloacting your load balancer puts everyone in the same country — configure trusted proxies and confirm with a test.isp, timezone, postalCode, or coordinates. Null-check every field you consume; don't assume city is populated for every country.GeoLite2-City.mmdb (or commercial equivalent) on disk. Set the path in config and schedule a recurring job (or use the official geoipupdate tool) to refresh the file — stale databases degrade accuracy.php artisan config:cache (and clear it first). Driver names in GEOIP_DRIVER must exactly match keys under services; typos yield a confusing "driver not supported" failure at resolution time, not boot time.strtoupper() before comparisons or database lookups.services entry accepts a custom driver class implementing the package's driver contract — that's the cleanest place to slot in an internal service without forking.How can I help you explore Laravel packages today?