Skip to content

Commit 40a7b4a

Browse files
author
Jens Wiese
committed
Implement MaxData GeoIP2 provider and database adapter
Update README.md, add requirements/suggest to composer.json.
1 parent 972eed7 commit 40a7b4a

File tree

6 files changed

+642
-2
lines changed

6 files changed

+642
-2
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Currently, there are the following adapters:
1818
* `GuzzleHttpAdapter` to use [Guzzle](https://github.com/guzzle/guzzle), PHP 5.3+ HTTP client and framework for building RESTful web service clients;
1919
* `SocketHttpAdapter` to use a [socket](http://www.php.net/manual/function.fsockopen.php);
2020
* `ZendHttpAdapter` to use [Zend Http Client](http://framework.zend.com/manual/2.0/en/modules/zend.http.client.html).
21+
* `GeoIP2DatabaseAdapter` to use [GeoIP2 Database Reader by MaxMind](https://github.com/maxmind/GeoIP2-php#database-reader).
2122

2223

2324
### Providers ###
@@ -49,6 +50,7 @@ Currently, there are many providers for the following APIs:
4950
* [GeoIPs](http://www.geoips.com/developer/geoips-api) as IP-Based geocoding provider;
5051
* [MaxMind web service](http://dev.maxmind.com/geoip/legacy/web-services) as IP-Based geocoding provider (City/ISP/Org and Omni services);
5152
* [MaxMind binary file](http://dev.maxmind.com/geoip/legacy/downloadable) as IP-Based geocoding provider;
53+
* [MaxMind GeoIP2 database file](http://www.maxmind.com/en/city) as IP-Based geocoding provider;
5254
* [Geonames](http://www.geonames.org/) as Place-Based geocoding and reverse geocoding provider;
5355
* [IpGeoBase](http://ipgeobase.ru/) as IP-Based geocoding provider (very accurate in Russia);
5456
* [Baidu](http://developer.baidu.com/map/geocoding-api.htm) as Address-Based geocoding and reverse geocoding provider (exclusively in China);
@@ -259,6 +261,14 @@ package must be installed.
259261
It is worth mentioning that this provider has **serious performance issues**, and should **not**
260262
be used in production. For more information, please read [issue #301](https://github.com/geocoder-php/Geocoder/issues/301).
261263

264+
### GeoIP2DatabaseProvider ###
265+
266+
The `GeoIP2DatabaseProvider` named `geoip2_database` is able to geocode **IPv4 and IPv6 addresses**
267+
only - it makes use of the MaxMind GeoIP2 databases.
268+
269+
It requires the [database file](http://dev.maxmind.com/geoip/geoip2/geolite2/), and the [geoip2/geoip2](https://packagist.org/packages/geoip2/geoip2) package must be installed.
270+
271+
This provider will only work with the corresponding `GeoIP2DatabaseAdapter`.
262272

263273
### GeonamesProvider ###
264274

composer.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,18 @@
1818
"kriswallsmith/buzz": "@stable",
1919
"guzzle/guzzle": "@stable",
2020
"zendframework/zend-http": "~2.1",
21-
"geoip/geoip": "~1.13"
21+
"geoip/geoip": "~1.13",
22+
"geoip2/geoip2": "~0.6",
23+
"mikey179/vfsStream": "v1.2.0"
2224
},
2325
"suggest": {
2426
"kriswallsmith/buzz": "Enabling Buzz allows you to use the BuzzHttpAdapter.",
2527
"ext-curl": "Enabling the curl extension allows you to use the CurlHttpAdapter.",
2628
"ext-geoip": "Enabling the geoip extension allows you to use the MaxMindProvider.",
2729
"guzzle/guzzle": "Enabling Guzzle allows you to use the GuzzleHttpAdapter.",
2830
"zendframework/zend-http": "Enabling Zend Http allows you to use the ZendHttpAdapter.",
29-
"geoip/geoip": "If you are going to use the MaxMindBinaryProvider (conflict with geoip extension)."
31+
"geoip/geoip": "If you are going to use the MaxMindBinaryProvider (conflict with geoip extension).",
32+
"geoip2/geoip2": "If you are going to use the GeoIP2DatabaseProvider."
3033
},
3134
"autoload": {
3235
"psr-0": { "Geocoder": "src/" }
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Geocoder package.
5+
* For the full copyright and license information, please view the LICENSE
6+
* file that was distributed with this source code.
7+
*
8+
* @license MIT License
9+
*/
10+
11+
namespace Geocoder\HttpAdapter;
12+
13+
use Geocoder\Exception\RuntimeException;
14+
use Geocoder\Exception\InvalidArgumentException;
15+
use Geocoder\Exception\UnsupportedException;
16+
use GeoIp2\Database\Reader;
17+
18+
/**
19+
* @author Jens Wiese <jens@howtrueisfalse.de>
20+
*/
21+
class GeoIP2DatabaseAdapter implements HttpAdapterInterface
22+
{
23+
/**
24+
* Database file types
25+
*/
26+
const GEOIP2_CITY = 'geoip2_city';
27+
const GEOIP2_COUNTRY = 'geoip2_country';
28+
29+
/**
30+
* @var string
31+
*/
32+
protected $dbFile;
33+
34+
/**
35+
* @var string
36+
*/
37+
protected $dbType;
38+
39+
/**
40+
* @var string
41+
*/
42+
protected $locale;
43+
44+
/**
45+
* @var Reader
46+
*/
47+
protected $dbReader;
48+
49+
/**
50+
* @param string $dbFile
51+
* @param string $dbType (e.g. self::GEOIP2_CITY)
52+
* @throws \Geocoder\Exception\RuntimeException
53+
* @throws \Geocoder\Exception\InvalidArgumentException
54+
*/
55+
public function __construct($dbFile, $dbType = self::GEOIP2_CITY)
56+
{
57+
if (false === class_exists('\GeoIp2\Database\Reader')) {
58+
throw new RuntimeException(sprintf("The %s requires maxmind's lib 'geoip2/geoip2'", __CLASS__));
59+
}
60+
61+
if (false === is_file($dbFile)) {
62+
throw new InvalidArgumentException(sprintf('Given MaxMind database file "%s" is not a file.', $dbFile));
63+
}
64+
65+
if (false === is_readable($dbFile)) {
66+
throw new InvalidArgumentException(sprintf('Given MaxMind database file "%s" is not readable.', $dbFile));
67+
}
68+
69+
$this->dbFile = $dbFile;
70+
$this->dbType = $dbType;
71+
}
72+
73+
/**
74+
* @param Reader $dbReader
75+
*/
76+
public function setDbReader(Reader $dbReader)
77+
{
78+
$this->dbReader = $dbReader;
79+
}
80+
81+
/**
82+
* @param string $locale
83+
* @return $this
84+
*/
85+
public function setLocale($locale)
86+
{
87+
$this->locale = $locale;
88+
89+
return $this;
90+
}
91+
92+
/**
93+
* @return string
94+
*/
95+
public function getLocale()
96+
{
97+
return $this->locale;
98+
}
99+
100+
/**
101+
* Destruct (e.g. database reader)
102+
*/
103+
public function __destruct()
104+
{
105+
$this->getDbReader()->close();
106+
}
107+
108+
/**
109+
* Returns the content fetched from a given resource.
110+
*
111+
* @param string $url (e.g. file://database?127.0.0.1)
112+
* @throws \Geocoder\Exception\UnsupportedException
113+
* @throws \Geocoder\Exception\InvalidArgumentException
114+
* @return string
115+
*/
116+
public function getContent($url)
117+
{
118+
$url = trim($url);
119+
120+
if (false === filter_var($url, FILTER_VALIDATE_URL)) {
121+
throw new InvalidArgumentException(
122+
sprintf('"%s" must be called with a valid url. Got "%s" instead.', __METHOD__, $url)
123+
);
124+
}
125+
126+
$ipAddress = parse_url($url, PHP_URL_QUERY);
127+
128+
if (false === filter_var($ipAddress, FILTER_VALIDATE_IP)) {
129+
throw new InvalidArgumentException('URL must contain a valid query-string (a IP address, 127.0.0.1 for instance)');
130+
}
131+
132+
switch ($this->dbType) {
133+
case self::GEOIP2_CITY:
134+
$result = $this->getDbReader()->city($ipAddress);
135+
break;
136+
default:
137+
throw new UnsupportedException(
138+
sprintf('Database type "%s" not implemented yet.', $this->dbType)
139+
);
140+
}
141+
142+
return json_encode($result);
143+
}
144+
145+
/**
146+
* Returns the name of the Adapter.
147+
*
148+
* @return string
149+
*/
150+
public function getName()
151+
{
152+
return 'maxmind_database';
153+
}
154+
155+
/**
156+
* Returns database reader
157+
*
158+
* @return Reader
159+
*/
160+
protected function getDbReader()
161+
{
162+
if (is_null($this->dbReader)) {
163+
$this->dbReader = new Reader($this->dbFile, $this->getLocale());
164+
}
165+
166+
return $this->dbReader;
167+
}
168+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Geocoder package.
5+
* For the full copyright and license information, please view the LICENSE
6+
* file that was distributed with this source code.
7+
*
8+
* @license MIT License
9+
*/
10+
11+
namespace Geocoder\Provider;
12+
13+
use Geocoder\Exception\NoResultException;
14+
use Geocoder\Exception\InvalidArgumentException;
15+
use Geocoder\Exception\RuntimeException;
16+
use Geocoder\Exception\UnsupportedException;
17+
use Geocoder\HttpAdapter\AdapterInterface;
18+
use Geocoder\HttpAdapter\GeoIP2DatabaseAdapter;
19+
use Geocoder\HttpAdapter\HttpAdapterInterface;
20+
use GeoIp2\Database\Reader;
21+
use GeoIp2\Exception\AddressNotFoundException;
22+
use GeoIp2\Model\City;
23+
24+
/**
25+
* @author Jens Wiese <jens@howtrueisfalse.de>
26+
*/
27+
class GeoIP2DatabaseProvider extends AbstractProvider implements ProviderInterface
28+
{
29+
/**
30+
* @var string
31+
*/
32+
protected $dbFile;
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function __construct(HttpAdapterInterface $adapter, $locale = 'en')
38+
{
39+
if (false === $adapter instanceof GeoIP2DatabaseAdapter) {
40+
throw new InvalidArgumentException(
41+
'GeoIP2DatabaseAdapter is needed in order to access the GeoIP2 databases.'
42+
);
43+
}
44+
45+
parent::__construct($adapter, $locale);
46+
}
47+
48+
/**
49+
* {@inheritDoc}
50+
*/
51+
public function getGeocodedData($address)
52+
{
53+
if (false === filter_var($address, FILTER_VALIDATE_IP)) {
54+
throw new UnsupportedException(sprintf('The %s does not support street addresses.', __CLASS__));
55+
}
56+
57+
if ('127.0.0.1' === $address) {
58+
return $this->getLocalhostDefaults();
59+
}
60+
61+
$result = json_decode($this->executeQuery($address));
62+
63+
return array($this->fixEncoding(array_merge($this->getDefaults(), array(
64+
'countryCode' => (isset($result->country->iso_code) ? $result->country->iso_code : null),
65+
'country' => (isset($result->country->names->{$this->locale}) ? $result->country->names->{$this->locale} : null),
66+
'city' => (isset($result->city->names->{$this->locale}) ? $result->city->names->{$this->locale} : null),
67+
'latitude' => (isset($result->location->latitude) ? $result->location->latitude : null),
68+
'longitude' => (isset($result->location->longitude) ? $result->location->longitude : null),
69+
'timezone' => (isset($result->location->timezone) ? $result->location->timezone : null),
70+
'zipcode' => (isset($result->location->postalcode) ? $result->location->postalcode : null),
71+
))));
72+
}
73+
74+
/**
75+
* {@inheritDoc}
76+
*/
77+
public function getReversedData(array $coordinates)
78+
{
79+
throw new UnsupportedException(sprintf('The %s is not able to do reverse geocoding.', __CLASS__));
80+
}
81+
82+
/**
83+
* {@inheritDoc}
84+
*/
85+
public function getName()
86+
{
87+
return 'geoip2_database';
88+
}
89+
90+
/**
91+
* @param string $address
92+
* @throws \Geocoder\Exception\NoResultException
93+
* @return City
94+
*/
95+
protected function executeQuery($address)
96+
{
97+
$uri = sprintf('file://database?%s', $address);
98+
99+
try {
100+
$result = $this->getAdapter()->setLocale($this->locale)->getContent($uri);
101+
} catch (AddressNotFoundException $e) {
102+
throw new NoResultException(sprintf('No results found for IP address %s', $address));
103+
}
104+
105+
return $result;
106+
}
107+
108+
}

0 commit comments

Comments
 (0)