Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ip2Location Laravel Laravel Package

ip2location/ip2location-laravel

Laravel wrapper for IP2Location that looks up geolocation data from an IP address, including country, region, city, coordinates, ZIP, timezone, ISP, and domain. Supports IPv4/IPv6 and multiple IP2Location BIN databases.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install the package

composer require ip2location/ip2location-laravel

Laravel 5.5+ auto-discovers the service provider and the IP2LocationLaravel facade alias. On older versions, register IP2Location\IP2LocationLaravel\IP2LocationLaravelServiceProvider in config/app.php and add the IP2LocationLaravel alias manually.

2. Publish the config

php artisan vendor:publish --provider="IP2Location\IP2LocationLaravel\IP2LocationLaravelServiceProvider"

This creates config/ip2location.php. Open it first — it contains only database paths, and the exact array keys vary by release, so read the published file rather than assuming.

3. Drop in a BIN database

The package ships no data — it's a thin wrapper around ip2location/ip2location-php, which reads IP2Location's proprietary .BIN files. Download a LITE database from ip2location.com (DB1 = country only, DB11 = country/region/city/coords/zip/timezone/ISP/domain/netspeed/area code/weather/MCC-MNC/brand/elevation/usage type) and place it in storage/app/ip2location/:

mkdir -p storage/app/ip2location
# put IP2LOCATION-LITE-DB11.BIN (and the .IPV6.BIN if you need v6) here

Then point the config at it (env-driven so staging/prod can differ):

// config/ip2location.php
'database' => [
    'ipv4' => env('IP2LOCATION_IPV4', storage_path('app/ip2location/IP2LOCATION-LITE-DB11.BIN')),
    'ipv6' => env('IP2LOCATION_IPV6', storage_path('app/ip2location/IP2LOCATION-LITE-DB11.IPV6.BIN')),
],

4. First use case — resolve the visitor's country

use IP2LocationLaravel;

$record = IP2LocationLaravel::get($request->ip(), 'bin');

// ['ip' => '8.8.8.8', 'countryCode' => 'US', 'countryName' => 'United States of America',
//  'regionName' => 'California', 'cityName' => 'Mountain View', 'latitude' => '37.405992',
//  'longitude' => '-122.078515', 'zipCode' => '94043', 'timeZone' => '-07:00', ...]
$country = $record['countryCode'];

That's the whole loop: request → IP → array → your logic. Everything else is what you build on top.

Where to look first: the get() return array is your real contract — dump it once against a known public IP (8.8.8.8) and against your own LAN IP to see exactly which keys your specific BIN file populates. Then read vendor/ip2location/ip2location-php/src/Database.php if you need field masks or memory-cache mode; the Laravel package itself is a few dozen lines.

Implementation Patterns

Wrap it in a service, not scattered facade calls

The facade is convenient but untestable at the seam. Put it behind one class that returns a typed DTO and does caching, sanitisation, and fallback in a single place:

final class GeoResolver
{
    public function forIp(?string $ip): ?GeoRecord
    {
        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()->addDays(7), function () use ($ip) {
            try {
                $raw = IP2LocationLaravel::get($ip, 'bin');
            } catch (\Throwable $e) {
                report($e);
                return null; // fail open — never 500 the request over geolocation
            }

            return GeoRecord::fromIp2Location($raw);
        });
    }
}

Bind GeoResolver in a service provider and inject it. Swap in a fake in tests via $this->instance(GeoResolver::class, ...) — no network, no BIN file needed in CI.

Request-scoped memoisation

A single page may need country for the locale, currency, and a shipping estimate. The facade re-reads the BIN file each call. Add a private array $memo = [] in your resolver, or use once() (PHP 8.1+/Laravel 11) so one request = one file read.

Middleware for geo-context

Resolve once at the edge of the request, stash it on the request, and share downstream:

public function handle(Request $request, Closure $next)
{
    $geo = app(GeoResolver::class)->forIp($request->ip());
    $request->attributes->set('geo', $geo);
    View::share('geo', $geo);   // or Inertia::share / a singleton binding
    return $next($request);
}

Then App::setLocale($geo?->countryToLocale() ?? config('app.locale')), or set currency/shipping defaults. This is by far the most common production use: locale and currency defaults, not blocking.

Tiered databases for cost/performance

Store DB1 (country, ~few MB) and DB11 (~50–400 MB) side by side. Use DB1 in hot paths (middleware, every request) and DB11 only on pages that need city/ISP/ZIP. A country lookup against DB1 is dramatically cheaper in page cache and I/O.

Analytics and enrichment off the request path

For logging geo data to a visits table, dispatch a queued job with the IP and let the worker resolve it. Don't add a BIN read to a write endpoint if you can avoid it:

LogVisit::dispatch($request->ip(), $request->path())->onQueue('analytics');

Human-readable mapping

usageType (DCH/ISP/COM/EDU/GOV/MIL/ORG/RES/CDN/SES), netSpeed (DIAL/DSL/T1/…), and addressType are codes. Ship an enum or config map to labels; don't render raw codes to users.

Finer control: bypass the wrapper

If you need field masks, memory-cache mode, or database version introspection, bind the underlying library yourself:

$this->app->singleton(\IP2Location\Database::class, fn () => new \IP2Location\Database(
    config('ip2location.database.ipv4'),
    \IP2Location\Database::FILE_IO
));

$db->lookup($ip, \IP2Location\Database::COUNTRY | \IP2Location\Database::CITY);

This is the intended extension point — the Laravel package is a convenience, not a constraint.

Gotchas and Tips

Private and reserved IPs return sentinel values, not errors. 127.0.0.1, 10.x, 192.168.x come back with '-' or "Invalid IP address" strings. Validate with FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE before lookup, and treat '-' as null everywhere. This is the #1 source of "why does my local dev show country -" confusion.

Fields outside your BIN file's tier return a literal warning string. The LITE DBs and lower-tier databases return text like "This parameter is unavailable for selected data file. Please upgrade the data file." for ISP, domain, elevation, etc. If you render it directly you'll leak that string into your UI. Always whitelist the fields your purchased tier actually supports.

IPv6 needs a separate file. Without the .IPV6.BIN in config, v6 visitors silently fall through to -. If you serve any mobile traffic, you need it — and the v6 files are much larger.

$request->ip() is only as good as your TrustProxies config. Behind Cloudflare/ALB/nginx you'll otherwise get the proxy's IP for 100% of traffic and a very consistent, very wrong country. Configure trusted proxies ($middleware->trustProxies(at: [...]) in Laravel 11+, or app/Http/Middleware/TrustProxies.php in older versions) and verify with a real request through the edge before shipping.

Latitude/longitude/elevation come back as strings. Cast to float/int before arithmetic or JSON responses — otherwise your frontend gets "37.405992" where it expects a number.

timeZone is a UTC offset ('-07:00'), not an IANA name. You cannot pass it to Carbon::setTimezone() directly. Convert it, or use it only for display, or derive the zone from country+region.

The BIN file goes stale. IP2Location ships updates monthly; a 6-month-old file gives measurably wrong ISP/usage-type data. Add a scheduled job or deploy step that re-downloads the BIN and restarts queue workers/Octane. Note that the wrapper holds an open file handle — a swapped file will not be picked up by a long-running worker until it restarts.

Never commit the BIN file. They're tens to hundreds of megabytes. Download at deploy time, exclude from git, and make sure storage/app/ip2location is writable by the app user. In container builds, bake the file into the image and pin the absolute path — a storage_path() cached via config:cache at build time will point at a path that may not exist in the runtime container.

Watch memory. FILE_IO (the safe default) is fast and light. MEMORY_CACHE loads the entire BIN into RAM per process — great latency, catastrophic under many FPM workers or Octane with a 400 MB file. Don't enable it without measuring.

Cache the result, not the object. Wrap lookups in Cache::remember($ip, $ttl). Because the package caches the Database instance as a singleton, you avoid re-opening the file anyway, but caching results also skips the index scan for repeat visitors. 24 hours–7 days is a sane TTL; IP-to-geo drifts slowly. Key by IP and database version if you swap DBs.

Don't make security decisions on it alone. Geo-blocking, fraud scoring, or GDPR-consent-by-country built purely on IP2Location will misfire on VPNs, corporate NAT, and mobile carriers. Use it as a signal, pair it with a fallback path, and always give users a way to correct their detected country. Also: IP-derived location is personal data under GDPR — if you persist it, it belongs in your retention policy.

The facade alias is easy to typo. It's IP2LocationLaravel, and the container binding is ip2location. If the alias isn't resolving (older Laravel, cached config from before install), use app('ip2location') and re-run php artisan config:clear.

The package is a thin wrapper with a small maintenance surface (~79 stars, but the release cadence is current). The real dependency risk is ip2location/ip2location-php — pin it and test after upgrades, since the lookup()/get() API and return-key casing have changed across major versions. If the wrapper ever lags, dropping to the underlying library is a five-line change, which is exactly why you should keep your own GeoResolver in front of it.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin