fof/geoip
FriendsOfFlarum GeoIP brings IP-based geolocation to Flarum, adding country flags to user profiles and posts via MaxMind GeoLite2. It helps communities see where members are from with configurable, lightweight display options.
fof/geoip is a Flarum extension (not a standalone Laravel package — Flarum runs on Illuminate components, so the service-container patterns will feel familiar) that resolves an IP address to location data. Install it like any Flarum extension:
composer require fof/geoip
php flarum migrate
php flarum cache:clear
Then enable it in Admin → Extensions → GeoIP and pick a driver. The plugin ships with multiple providers (MaxMind GeoLite2, IP-API, and similar) and the admin panel exposes provider-specific settings under the fof-geoip.* settings namespace.
Where to look first:
extend.php — shows which routes, settings, and bindings are registered.src/GeoIPServiceProvider.php (or equivalent) — how the GeoIP contract is bound into the container.src/Drivers/ — each provider (MaxMind/IP-API/etc.) implements the same contract. Reading one driver tells you the shape of the return values and error handling.First use case: resolve a visitor's country inside a controller, listener, or middleware:
use FoF\GeoIP\GeoIP;
$country = app(GeoIP::class)->getCountry($request->ip()); // e.g. "DE" or null
That single call is the 90% entry point — everything else in the extension exists to make that call work reliably (driver selection, database updates, caching).
Inject the contract, never a concrete driver. Bind through the GeoIP interface so switching providers in admin doesn't break your code:
public function __construct(protected GeoIP $geoip) {}
Wrap lookups in a caching decorator. Every driver either does a network call (IP-API) or a database hit (MaxMind). A per-IP cache with a sane TTL is the single biggest performance win:
$country = Cache::remember("geoip:{$ip}", now()->addDays(30), fn () =>
$this->geoip->getCountry($ip)
);
Geolocation is effectively static for a given IP, so 30–90 days is reasonable.
Enrich asynchronously when it isn't on the request path. If you're tagging users at registration or attaching country to posts, don't block the response — dispatch a queued job:
class ResolveUserCountry implements ShouldQueue
{
public function handle(GeoIP $geoip): void
{
if ($country = $geoip->getCountry($this->user->last_ip)) {
$this->user->save(['geoip_country' => $country]);
}
}
}
Custom drivers. If your providers aren't covered, implement the contract and register it — either by binding your class in extend.php or by extending the driver registry the extension exposes. This is the intended extension point: keep lookup logic in the driver and let the contract handle the rest.
Pair it with the ecosystem. The most common real-world use isn't calling GeoIP directly but letting it feed other FriendsOfFlarum extensions — spam blocking by country, IP-ban tooling, registration restrictions. In those cases you rarely touch the API at all; you configure the settings and let the consuming extension call the binding.
Keep a "test" driver. For feature tests, bind a fake implementation of the contract in your test case so nothing hits the network:
$this->app->instance(GeoIP::class, new FakeGeoIP());
127.0.0.1, 10.0.0.0/8, 192.168.x.x, and IPv6 loopback will yield null (or worse, throw depending on the driver). Always null-check and have a fallback path.$request->ip() returns the proxy. Configure trusted proxies at the framework level or read CF-Connecting-IP, otherwise every "visitor country" is your CDN's datacenter.null. Treat external-API drivers as best-effort.try { } catch and fail open. A third-party API outage should never 500 a login.fof-geoip.*) — grep the driver for Settings::get to find the exact names.composer.json, review the driver code for your chosen provider, and expect to read source rather than docs. The contract is stable enough to abstract behind your own service if you later swap providers.How can I help you explore Laravel packages today?