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

Geoip2 Bundle Laravel Package

huluti/geoip2-bundle

Symfony bundle for MaxMind GeoIP2 that resolves IP addresses to geographic data such as country, city, and coordinates using local databases, with service integration for looking up visitor locations in applications.

View on GitHub
Deep Wiki
Context7

Getting Started

What changed in 3.0.0 — read this before you install. This is a major release and the first release under the fork (the package descends from gpslab/geoip2). The headline items for a consumer are: PHP 8.4/8.5 support, old PHP and Symfony versions dropped, and the legacy "geoip v2" dependency line dropped. That last one is the one to verify yourself rather than assume — open the released composer.json and check the actual constraint on the underlying GeoIP library (and on symfony/*) before you plan an upgrade. The UPGRADE-3.0.md file shipped with the release is the authoritative migration doc; read it first, then the bundle's DI extension class, then the README. (The PHPUnit/PHPStan bumps in the notes are dev-only — interesting as a signal that the fork is maintained with CI on modern runtimes, not as something you act on.)

Then the framework reality check, which has not changed. huluti/geoip2-bundle is a Symfony bundle, not a Laravel package. No service provider, no facade, no config/geoip.php. In Laravel you will almost certainly install its dependency directly (geoip2/geoip2) and wire it yourself. Treat this assessment as: what this bundle teaches you about structuring GeoIP2 in a PHP app, and how to reproduce that structure in Laravel.

If you are in a Symfony app, the minimal path:

composer require huluti/geoip2-bundle:^3.0

Register the bundle in config/bundles.php, then point it at your MaxMind database:

# config/packages/huluti_geoip2.yaml
huluti_geoip2:
    database_path: '%kernel.project_dir%/var/GeoLite2-City.mmdb'

Because 3.0.0 is a fork re-release, do not copy the config root or service IDs from pre-3.x tutorials — confirm both against the installed bundle (the extension class + services.yaml). Wrong keys in Symfony usually fail silently (defaults applied), which is far worse than a hard error.

You still need the .mmdb file. GeoLite2 requires a free MaxMind account and license key; the file is not bundled.

First use case: resolve a request IP to a country/city for geo-targeted content, currency/locale defaults, or a coarse fraud signal. Inject the reader service and call ->city($ip):

public function __construct(private \GeoIp2\Database\Reader $reader) {}

public function locate(string $ip): array
{
    $record = $this->reader->city($ip);
    return [
        'country' => $record->country->isoCode,
        'city'    => $record->city->name,
        'lat'     => $record->location->latitude,
        'lng'     => $record->location->longitude,
    ];
}

This snippet is stable across the GeoIP2 major versions, but if you're on a freshly-upgraded underlying library, sanity-check the record accessors once against the docs for the version you actually installed.

Implementation Patterns

Port the bundle's shape to Laravel. Its whole job is: read config, construct a Reader, expose it as a singleton. Do the same:

// app/Providers/GeoIpServiceProvider.php
public function register(): void
{
    $this->app->singleton(\GeoIp2\Database\Reader::class, fn () => new \GeoIp2\Database\Reader(
        config('services.geoip.database', storage_path('app/geoip/GeoLite2-City.mmdb'))
    ));
}

Bind as a singleton — the Reader memory-maps the file and should be built once per process, not per call. Mirror the bundle's 3.0 constraints in your own composer.json (PHP and the underlying GeoIP library) so your Laravel app and any Symfony service you share code with don't drift onto different library majors.

Middleware pattern (the day-to-day one). Resolve and stash once per request instead of re-reading in every controller:

public function handle($request, Closure $next)
{
    $ip = $request->ip();
    $request->attributes->set('geo', $this->safeCity($ip)); // null on failure
    return $next($request);
}

Guard every lookup. AddressNotFoundException is thrown for private/reserved IPs (127.0.0.1, 10.x, 192.168.x, ::1). In local dev every request throws, so wrap and return null:

try {
    return $reader->city($ip);
} catch (\GeoIp2\Exception\AddressNotFoundException $e) {
    return null; // localhost, LAN, reserved ranges
}

Cache per-IP results. MMDB lookups are microseconds, but in hot paths Cache::remember("geo:{$ip}", 3600, ...) is cheap insurance. Don't cache null/private-IP failures forever if you test from varying local IPs.

Database reader vs. web service. Depending on version the bundle exposes the local Reader and/or MaxMind's WebService\Client (user_id + license_key); the 3.0 notes' dependency drop is a good reason to re-check which providers you still get. Default to the database reader — no network hop, no per-query cost, no rate limits. Use the web service only for data GeoLite2 lacks (Enterprise, anonymizer/ISP datasets) and when you can accept the latency.

Keep lookups at the edge of your domain. Resolve to a small value object (Country, Coordinates) in a dedicated service and keep GeoIp2\* types out of controllers/models. That makes the reader mockable and lets you swap providers without touching business logic — extra valuable here, given the package just changed maintainership and jumped a major version.

Update pipeline. GeoLite2 refreshes weekly (city) / monthly-ish (country). Wire a scheduled command to re-download the .mmdb, swap it atomically, then recycle workers so the new file is memory-mapped. Stale files silently serve stale geolocation — nothing errors.

Gotchas and Tips

  • 3.0.0 is a breaking release, even if you weren't using anything fancy. Old PHP and Symfony versions are dropped, and the legacy geoip v2 line is gone. If your app (or a shared library) is pinned to an older PHP or Symfony, composer require will fail or force a wide upgrade — check constraints before bumping, and run UPGRADE-3.0.md top to bottom rather than improvising.
  • Fork churn is a real migration risk. The package identity moved (gpslab → huluti fork) and the first release under it is a major. Expect renamed config keys, service aliases, or provider wiring versus any pre-3.x article, Stack Overflow answer, or vendor code you copied. Verify against the installed bundle's DI extension and services.yaml, not the internet.
  • It is still not a Laravel package. composer require huluti/geoip2-bundle will not give you a usable service provider; you'll get class-not-found or Symfony-only services in a container Laravel never builds. Use geoip2/geoip2 directly.
  • The .mmdb is not included. Fresh installs fail at runtime with InvalidDatabaseException or file-not-found if the path is wrong. Add a bootstrap check that verifies the file exists and fails loudly at boot, not on the first lookup.
  • Licensing/attribution. GeoLite2 needs a MaxMind account and EULA acceptance; data must be attributed per their terms. Automated downloads hit download.maxmind.com with account_id/license_key. Don't commit the .mmdb — tens of MB, versioned by date.
  • Private/reserved IPs throw — in tests and queue workers too, not just HTTP. Your safe wrapper should be the only way callers touch the reader.
  • IPv6 and proxies. Behind a load balancer/CDN, $request->ip() is the proxy; configure TrustProxies (Laravel) / trusted proxies (Symfony) or you'll geolocate your own datacenter. Normalize/validate the IP first — a malformed string raises InvalidArgumentException.
  • Singleton discipline. new Reader(...) per call re-opens and re-maps the file — measurable under load. Bind once; in long-running workers (Octane, queue daemons) restart after a data update. Now that PHP 8.4/8.5 are explicitly supported, this matters more, not less: long-lived workers on new runtimes are exactly where a stale mmap bites.
  • Accuracy expectations. GeoLite2 city accuracy is fine for country/region and coarse city, not precise location — lat/lng can be tens of km off, VPN/proxy users resolve to the VPN. Treat it as a hint, never as a security-grade signal.
  • Testing. Wrap the reader in a small interface and mock it; never hit the real reader in unit tests. Assert on one known public IP (8.8.8.8 → US) in a single integration test, skipped when the .mmdb is absent.
  • Small package, thin docs, now maintained. It's still a low-traffic MIT bundle, but the 3.0.0 tooling bumps (PHPUnit/PHPStan) and PHP 8.4/8.5 CI mean it tracks modern runtimes. Pin ^3.0, read UPGRADE-3.0.md, and keep treating the patterns — singleton reader, config-driven path, safe wrapper — as the durable takeaway rather than betting your architecture on the bundle.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core