GeoIP2 官方 PHP SDK 是纯 Composer 包,无需编译扩展,需手动下载 GeoLite2-City.mmdb 并用单例模式加载,查询时必须捕获 AddressNotFoundException。

直接用 Composer 装 geoip2/geoip2,不是装“PHP 扩展”
很多人搜“PHP 安装 GeoIP2 扩展”,结果去编译 php-pecl-geoip 或折腾 geoip.so —— 这是旧版 GeoIP Legacy 的路子,完全不兼容 GeoIP2 的 .mmdb 数据库。GeoIP2 官方 PHP SDK 是纯 Composer 包,无需系统级扩展,也不读 GeoIP.dat。确认这点能避免后续所有路径、格式、函数名报错。
执行即可:
composer require geoip2/geoip2
它会自动注册自动加载,new \GeoIp2\Database\Reader(...) 就能用。但注意:geoip2/geoip2 只是 SDK,不带数据文件,下一步必须手动处理数据库。
下载 GeoLite2-City.mmdb 并放对位置
MaxMind 自 2019 年起已停更旧版 .dat,现在只提供 .mmdb(二进制 MaxMind DB 格式)。不下载这个,Reader 实例化时必抛 MaxMind\Db\Reader\Exception\InvalidDatabaseException。
立即学习“PHP免费学习笔记(深入)”;
- 去 MaxMind 免费页 注册账号,获取 License Key
- 用
curl下载(推荐放storage/geoip/):
mkdir -p storage/geoip curl -O https://downloads.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz tar -xzf GeoLite2-City.tar.gz --strip-components=1 -C storage/geoip/ GeoLite2-City_*/GeoLite2-City.mmdb
关键点:
-
base_path('storage/geoip/GeoLite2-City.mmdb')必须是绝对路径,相对路径如./storage/geoip/...会失败 - 文件权限设为
644,Web 进程需有读取权;SELinux 启用时可能需额外赋权:chcon -t httpd_sys_rw_content_t storage/geoip/GeoLite2-City.mmdb - 别用旧版
GeoLiteCity.dat,geoip2/geoip2v3+ 完全不识别它
Laravel 中 Reader 必须单例绑定,不能 new 在控制器里
每次 HTTP 请求都 new \GeoIp2\Database\Reader(...),等于反复打开同一 .mmdb 文件,性能差、易触发 open_basedir 限制、还可能因文件句柄耗尽报错。
正确做法是在 AppServiceProvider::boot() 中绑定单例:
use GeoIp2\Database\Reader;
use Illuminate\Support\Facades\App;
public function boot()
{
App::singleton(Reader::class, function () {
return new Reader(base_path('storage/geoip/GeoLite2-City.mmdb'));
});
}
然后在控制器中通过依赖注入或 app(Reader::class) 获取实例。这样整个请求生命周期只打开一次文件。
如果你用的是非 Laravel 项目,至少把 Reader 实例缓存到全局变量或静态属性中,避免重复构造。
查询时必须兜底 AddressNotFoundException
内网 IP(如 127.0.0.1、192.168.x.x)、私有地址段、未分配 IPv6 地址,查 city() 或 country() 都会直接抛 \GeoIp2\Exception\AddressNotFoundException —— 这不是错误,是设计行为,不 catch 会导致 500。
示例写法:
try {
$record = app(\GeoIp2\Database\Reader::class)->city($ip);
$country = $record->country->name ?? 'Unknown';
$city = $record->city->name ?? 'Unknown';
} catch (\GeoIp2\Exception\AddressNotFoundException $e) {
// 返回默认值或跳过地理逻辑
$country = 'Private';
$city = 'Local';
}
容易被忽略的是:即使你传的是公网 IP,MaxMind 数据库也可能没覆盖(尤其新分配的 ASN),所以异常捕获不是“以防万一”,而是每处调用都得写。



















