arcanedev/geo-ip
Laravel package for IP geolocation with a simple, unified API. Supports multiple providers such as MaxMind and IP2Location, letting you look up country, city, coordinates, and other location data from an IP address.
Install the package and pull in its MaxMind dependency:
composer require arcanedev/geo-ip
On Laravel 5.5+ the Arcanedev\GeoIP\GeoIPServiceProvider and the GeoIP facade are auto-discovered. On older apps add them manually to config/app.php:
'providers' => [
Arcanedev\GeoIP\GeoIPServiceProvider::class,
],
'aliases' => [
'GeoIP' => Arcanedev\GeoIP\Facades\GeoIP::class,
],
Publish the config and set up the database:
php artisan vendor:publish --provider="Arcanedev\GeoIP\GeoIPServiceProvider" --tag=config
php artisan geoip:update # downloads the MaxMind DB using your license key
Then set in .env:
MAXMIND_LICENSE_KEY=your-key
MAXMIND_USER_ID=your-account-id
First real use case — resolve the visitor's location in a controller or middleware:
$location = GeoIP::getLocation(request()->ip());
$location->isoCode; // "US"
$location->country; // "United States"
$location->city; // "Mountain View"
$location->timezone; // "America/Los_Angeles"
$location->default; // true if the lookup failed
Where to look first:
config/geoip.php — drivers (maxmind, ipapi), database_path, license_key, user_id, and the default_location fallback array.Arcanedev\GeoIP\GeoIP — the singleton behind the facade, exposes getLocation() and driver accessors.Arcanedev\GeoIP\Entities\Location — the value object returned by lookups (toArray() / toJson() for serialization).Middleware that tags every request. Resolve once and stash the result so downstream code doesn't re-read the mmdb file:
public function handle($request, Closure $next)
{
$request->attributes->set('geo', GeoIP::getLocation($request->ip()));
return $next($request);
}
Locale / timezone selection. The most common day-to-day win — pick a language or display timezone from the country code, with a safe default:
$locale = in_array($location->isoCode, ['FR', 'BE', 'CA']) ? 'fr' : 'en';
app()->setLocale($locale);
Currency, tax, and shipping logic. Country-level lookups are cheap and stable; use isoCode for VAT rules, price display, and shipping eligibility. Keep country logic in a config map rather than scattering if statements across controllers.
Geo-restrictions / compliance. Gate content or features by country in a policy or middleware:
abort_unless(in_array($geo->isoCode, config('geo.allowed_countries')), 403);
Caching lookups. Both drivers benefit — MaxMind hits the filesystem, the ipapi driver makes a synchronous HTTP call:
$location = Cache::remember("geoip:{$ip}", now()->addDay(), fn () => GeoIP::getLocation($ip));
Storing on the user/order record. Persist isoCode, country, and city at signup or checkout rather than re-resolving later — IPs change and the DB is refreshed weekly.
Fallback handling. config('geoip.default_location') supplies the payload used for private/invalid IPs and failed lookups. Always branch on $location->default (or a null isoCode) before treating a value as authoritative:
if ($location->default) {
// local dev, health checks, internal traffic
}
Testing. The facade resolves a container singleton named geoip; swap it in tests so nothing touches the mmdb file or network:
$this->app->instance('geoip', Mockery::mock(GeoIP::class, function ($m) {
$m->shouldReceive('getLocation')->andReturn(new Location([...]));
}));
Alternatively set default_location in the test config and let every lookup fall back to it.
Serialization for APIs. $location->toArray() is convenient for JSON responses and logging; toJson() avoids a second encode step if you're returning it verbatim.
geoip2/geoip2 and arcanedev/support versions can collide with modern Symfony/Laravel constraints. On Laravel 9+ it's often simpler to use geoip2/geoip2 directly — this package is a thin facade over it, so you lose very little.php artisan geoip:update (or a cron that re-downloads the mmdb) — nothing errors when data is old, results just drift.storage/app/geoip/, and changes on every update. Add the directory to .gitignore and provision it in your deploy pipeline, or run geoip:update as a release step.127.0.0.1, 10.x, 192.168.x and friends return the default location. Local development will always look like default: true unless you stub it.Location uses magic property access; $location->countryCode (wrong) returns null rather than throwing. Confirm field names with $location->toArray() — it's isoCode, not countryCode.ipapi driver is not production-grade. It's a synchronous call to a free public endpoint with rate limits and (historically) no HTTPS on the free tier. Use it for prototypes only; use maxmind with a local DB in production.geoip:update. Restart workers as part of the update step.config:cache freezes the path. If database_path is env-driven, remember to rebuild the config cache on deploy or lookups will point at the old file.try/catch (or rescue()) when a location is nice-to-have and shouldn't 500 the request.GeoLite2-City.mmdb for GeoLite2-Country.mmdb if all your logic keys off isoCode — much smaller file, faster reads.default_location and driver config, bind your own implementation of the GeoIP contract to add caching or a corporate proxy/VPN header strategy, or add a custom driver alongside maxmind/ipapi if you need a paid vendor.How can I help you explore Laravel packages today?