Реалізація автоопределення валюти по GeoIP для сайту
GeoIP-определення валюти — доповнення до системи мультивалютності, яка знімає з користувача необхідність самостійно вибирати валюту при першому візиті. Покупець з Беларусі бачить ціни в BYN, з Німеччини — в EUR, з США — в USD. Реалізація простіше, ніж здається, але є нюанси з точністю баз, кешуванням та уважанням до вибору користувача.
Як працює GeoIP
IP-адреса → країна → валюта. Ланцюжок з двох кроків.
Крок 1: IP → країна. Використовуються бази геолокації. Основні варіанти:
| База | Тип | Точність за країною | Стоимость |
|---|---|---|---|
| MaxMind GeoLite2 | Локальна MMDB | ~95–99% | Безплатно (реєстрація) |
| MaxMind GeoIP2 | Локальна + API | ~99%+ | Платно |
| ip-api.com | HTTP API | ~98% | Безплатно (1000/хв) |
| ipinfo.io | HTTP API | ~99% | Freemium |
| DB-IP | Локальна | ~95% | Freemium |
Для більшості проектів MaxMind GeoLite2 — оптимальний вибір: локальна база не залежить від зовнішніх сервісів та не сповільнює запити.
Крок 2: країна → валюта. Статична таблиця відповідностей ISO 3166-1 → ISO 4217.
Установка MaxMind GeoLite2
composer require geoip2/geoip2
База оновлюється MaxMind кожні вівторок та п'ятницю. Для автоматичного обновлення використовується утиліта geoipupdate:
# /etc/GeoIP.conf
AccountID 123456
LicenseKey your_license_key
EditionIDs GeoLite2-Country
DatabaseDirectory /var/lib/GeoIP
# cron кожну середу та суботу
0 3 * * 3,6 /usr/local/bin/geoipupdate
Сервіс определення валюти
class GeoIpCurrencyDetector
{
private Reader $geoIpReader;
public function __construct()
{
$this->geoIpReader = new Reader(
storage_path('app/geoip/GeoLite2-Country.mmdb')
);
}
public function detect(string $ip): ?string
{
// Пропускаємо приватні та зарезервовані діапазони
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return null;
}
try {
$record = $this->geoIpReader->country($ip);
$countryCode = $record->country->isoCode; // 'BY', 'RU', 'DE' та т.д.
return $this->mapCountryToCurrency($countryCode);
} catch (AddressNotFoundException) {
return null;
}
}
private function mapCountryToCurrency(string $countryCode): ?string
{
$map = config('geoip.country_currency_map');
return $map[$countryCode] ?? null;
}
}
Конфіг country_currency_map — це масив з ~250 країнами. Ключові записи:
// config/geoip.php
return [
'country_currency_map' => [
'BY' => 'BYN',
'RU' => 'RUB',
'UA' => 'UAH',
'KZ' => 'KZT',
'US' => 'USD',
'CA' => 'CAD',
'GB' => 'GBP',
'DE' => 'EUR', 'FR' => 'EUR', 'IT' => 'EUR', 'ES' => 'EUR',
'PL' => 'PLN',
'CZ' => 'CZK',
'CN' => 'CNY',
'JP' => 'JPY',
// ...та ще ~200 країн
],
'fallback_currency' => 'USD',
];
Кеширование результату
Определення за IP — швидка операція (локальна база, ~0.5 мс), але кешувати всі рівно варто: захист від повторних операцій читання файлу при кожному запиті.
public function detectCached(string $ip): ?string
{
$cacheKey = 'geoip:' . md5($ip);
return Cache::remember($cacheKey, now()->addDay(), function () use ($ip) {
return $this->detect($ip);
});
}
TTL 24 години — баланс між актуальністю (користувач не змінює країну кожну годину) та точністю (VPN-перемикання підхопиться на наступний день).
Інтеграція в middleware
class ResolveCurrencyFromGeoIp
{
public function handle(Request $request, Closure $next): Response
{
// Якщо користувач уже зробив явний вибір — не перебиваємо
if ($this->hasExplicitChoice($request)) {
return $next($request);
}
$ip = $request->ip();
// Враховуємо проксі та балансувальники
if ($request->header('CF-Connecting-IP')) {
$ip = $request->header('CF-Connecting-IP'); // Cloudflare
} elseif ($request->header('X-Real-IP')) {
$ip = $request->header('X-Real-IP'); // nginx proxy_pass
}
$currency = $this->detector->detectCached($ip);
if ($currency && $this->isSupportedCurrency($currency)) {
session(['auto_currency' => $currency]);
Cookie::queue('preferred_currency', $currency, 60 * 24 * 90);
}
return $next($request);
}
private function hasExplicitChoice(Request $request): bool
{
// Користувач явно переключав валюту
return session()->has('explicit_currency_choice')
|| $request->user()?->preferred_currency;
}
}
Обробка IP за проксі
Проблеми з IP виникають при:
- Cloudflare — реальний IP у
CF-Connecting-IP - nginx reverse proxy — реальний IP у
X-Real-IPабоX-Forwarded-For - AWS ELB —
X-Forwarded-For(перший в списку)
Правильна конфігурація Laravel через TrustProxies middleware:
// app/Http/Middleware/TrustProxies.php
protected $proxies = '*'; // або конкретні IP балансувальників
protected $headers = Request::HEADER_X_FORWARDED_FOR
| Request::HEADER_X_FORWARDED_HOST
| Request::HEADER_X_FORWARDED_PORT
| Request::HEADER_X_FORWARDED_PROTO;
Після цього $request->ip() повертає коректний клієнтський IP.
UX: повідомлення про автоопределення
Гарна практика — показати користувачу тост/баннер при першому автоопределенні:
Ми визначили, що ви з Беларусі. Ціни показані в BYN. Змінити валюту →
Баннер показується один раз (флаг в sessionStorage) та містить швидку ссилку для смени валюти. Це уважання до користувача: автоматика допомагає, а не навязує.
Коли GeoIP не працює
- VPN/проксі: користувач з RU виглядає як US → показується USD. Рішення: ссилка «Змінити валюту» завжди доступна.
- Корпоративні мережі: IP зареєстрований в іншій країні. Аналогічно.
- Tor: exit-нода у випадковій країні. Fallback на валюту за умовчанням.
- IPv6: база GeoLite2-Country підтримує IPv6 починаючи з версії 2020+. Переконайтесь, що використовується актуальна версія.
Тестування
Для локального тестування з фіктивними IP:
// В тестах або локальному окруженні
if (app()->environment('local')) {
$ip = config('geoip.test_ip', '178.124.0.1'); // белорусский IP
}
Для автотестів — mock сервісу:
$this->mock(GeoIpCurrencyDetector::class, function ($mock) {
$mock->shouldReceive('detectCached')->andReturn('BYN');
});
Терміни реалізації
- Установка GeoLite2 + базове определення + маппінг країн: 1 день
- Middleware + кеширование + інтеграція з системою мультивалютності: 1 день
- UX-повідомлення + обробка явного вибору: 0.5 дня
- Автообновлення бази (cron + geoipupdate): 0.5 дня
Итого: 2–3 дні при умові, що система мультивалютності вже реалізована.







