Files
ladill-link/app/Support/Link/LinkGeoResolver.php
T
isaaccladandCursor 344bf76391
Deploy Ladill Link / deploy (push) Successful in 39s
Add GeoIP country fallback for link click analytics.
Resolve countries from CDN headers first, then MaxMind GeoLite2 via visitor IP, with an artisan command to download and refresh the database.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 23:36:01 +00:00

112 lines
2.8 KiB
PHP

<?php
namespace App\Support\Link;
use GeoIp2\Database\Reader;
use GeoIp2\Exception\AddressNotFoundException;
use Illuminate\Http\Request;
final class LinkGeoResolver
{
/** @var list<string> */
private const HEADER_KEYS = ['CF-IPCountry', 'X-Country-Code', 'CloudFront-Viewer-Country'];
private static ?Reader $reader = null;
public static function countryCode(Request $request): ?string
{
if ($fromHeader = self::countryFromHeaders($request)) {
return $fromHeader;
}
return self::countryFromIp($request->ip());
}
public static function countryFromIp(?string $ip): ?string
{
if (! self::isPublicIp($ip)) {
return null;
}
$reader = self::reader();
if ($reader === null) {
return null;
}
try {
$code = strtoupper((string) ($reader->country((string) $ip)->country->isoCode ?? ''));
return strlen($code) === 2 && ! in_array($code, ['XX', 'T1'], true) ? $code : null;
} catch (AddressNotFoundException) {
return null;
} catch (\Throwable) {
return null;
}
}
public static function countryLabel(?string $code): string
{
if ($code === null || $code === '') {
return 'Unknown';
}
if (function_exists('locale_get_display_region')) {
$name = locale_get_display_region('_'.$code, 'en');
return is_string($name) && $name !== '' ? $name : $code;
}
return $code;
}
/** Visible for tests. */
public static function resetReader(): void
{
self::$reader = null;
}
private static function countryFromHeaders(Request $request): ?string
{
foreach (self::HEADER_KEYS as $header) {
$value = $request->header($header);
if (! is_string($value) || strlen($value) !== 2) {
continue;
}
$code = strtoupper($value);
if (! in_array($code, ['XX', 'T1'], true)) {
return $code;
}
}
return null;
}
private static function isPublicIp(?string $ip): bool
{
return is_string($ip)
&& $ip !== ''
&& filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
}
private static function reader(): ?Reader
{
if (self::$reader !== null) {
return self::$reader;
}
$path = (string) config('link.geoip_database', '');
if ($path === '' || ! is_readable($path)) {
return null;
}
try {
self::$reader = new Reader($path);
return self::$reader;
} catch (\Throwable) {
return null;
}
}
}