ip2location/ip2location-php
IP2Location PHP library for looking up IP addresses: country, region, city, coordinates, ZIP, ISP, domain, connection type, and more via BIN databases or the web service. Supports IPv4/IPv6 and works standalone or with Laravel.
Install the library and provision a database file — there is no Laravel service provider, facade, or config file to publish.
composer require ip2location/ip2location-php
.BIN file new IP2Location($path) throws.storage/app/ip2location/ and never commit it. LITE DB11 is tens of MB; commercial DBs are larger. Add a deploy/download step or mount it from a volume.AppServiceProvider::register():use IP2Location\IP2Location;
$this->app->singleton(IP2Location::class, function () {
return new IP2Location(storage_path(config('services.ip2location.bin')));
});
$record = app(IP2Location::class)->getAll(request()->ip());
if ($record && $record['countryCode'] === 'DE') {
app()->setLocale('de');
}
Look first at the package README’s list of getCountry(), getCity(), getAll(), and the WebService class — those cover 90% of Laravel use cases (localization, geo-blocking, currency, analytics enrichment, fraud checks). If you want a Laravel-native wrapper instead of hand-wiring, check whether ip2location/ip2location-laravel fits your version before building your own.
Wrap it in your own service. Because the package has no Laravel integration, create App\Services\GeoIpService that owns IP normalization, the IP2Location instance, and caching. Inject that service everywhere instead of the raw class. This gives you one place to swap providers later (MaxMind, Cloudflare headers, a hosted API) and one place to fake in tests.
Resolve the real client IP, not request()->ip() blindly. Behind load balancers, Cloudflare, or Laravel Octane, the socket IP is often the proxy. Configure TrustProxies correctly and consider a helper that prefers CF-Connecting-IP, then X-Forwarded-For, then request()->ip(). Reject private/reserved ranges (127.0.0.1, 10/8, 172.16/12, 192.168/16, ::1) before lookup; the library returns no usable record for them.
Cache per IP. BIN lookups are fast but not free, and the same IPs recur. Use Cache::remember("geoip:{$ip}", now()->addDay(), fn () => $service->lookup($ip)). For authenticated users, cache on user_id instead. Invalidation is easy because IP-to-location changes slowly.
Middleware for locale/currency/geo-restriction. Resolve the country once per request in middleware, store it on the request ($request->attributes->set('geo', $record)) or in a scoped container binding, then let controllers, Blade/Inertia shared props, and form requests read it. Avoid calling the library repeatedly in views.
Batch enrichment in jobs. For imports, order backfills, or analytics pipelines, inject the service into a queued job and chunk records. Use getAll() once per IP rather than ten separate getter calls. Keep the singleton alive within a worker; for very large imports consider one job per chunk so memory and retries stay predictable.
Choose the right database edition. DB1 (country) is small and fast; DB3 adds region/city; DB11 adds ISP, domain, timezone, mobile, usage type, IAB category. Don’t load DB11 if you only need country — the file size and deploy cost matter. If you need multiple editions, create separate named singletons and pick per feature.
Use the WebService class as a fallback. IP2Location\WebService calls the hosted API with an API key. Good when you cannot ship a BIN file, need fields your local DB lacks, or want a failover. Put the key in config/services.php and env, and cache aggressively because API plans have quotas.
Validation and form requests. Country-aware validation rules (phone format, postal code, tax/VAT rules) can consume the resolved country. Resolve geo data in a base form request or middleware so validation stays declarative.
Testing. Bind a fake implementation of your GeoIpService in tests. For integration tests against the real BIN, use known stable IPs and assert only fields your edition actually provides. Never assert city-level accuracy from the LITE database — it is deliberately coarse.
config/ip2location.php, no facade. Everything above is manual wiring; budget for that or use a community wrapper.storage_path() or base_path() with a config value, never public_path(). The BIN file must be readable by the PHP user and excluded from git. On containerized deploys, download it during build or mount it; otherwise the constructor throws on first request.IP2Location opens the file. Register it as a singleton to avoid reopening per lookup. Under Octane, a long-lived singleton keeps a file handle across requests — if you replace the BIN during a deploy, restart workers or make the service re-open on file mtime change.countryCode in a Blade template is a common source of notices.getAll() can expose different key names (countryCode vs. country_code depending on version and whether you use the WebService). Read the README for the exact keys of your installed version and edition, and normalize in your wrapper.TrustProxies and test from a real external IP, not localhost.getDatabaseVersion() if available) so you can debug stale results.getAll() once per IP. For high-volume analytics, consider a columnar/offline join rather than per-request PHP lookups.GeoLocator) with methods you actually use (country, city, timezone, coordinates). That keeps your app independent of IP2Location’s array shapes and lets you test, swap providers, and add caching without touching call sites.How can I help you explore Laravel packages today?