Skip to content

Commit

Permalink
Merge pull request #303 from jenswiese/geoip2
Browse files Browse the repository at this point in the history
Implement MaxData GeoIP2 provider and database adapter
  • Loading branch information
willdurand committed Jun 13, 2014
2 parents 1b385bb + 6b5bbd8 commit 12b862d
Show file tree
Hide file tree
Showing 6 changed files with 624 additions and 2 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Currently, there are the following adapters:
* `GuzzleHttpAdapter` to use [Guzzle](https://github.com/guzzle/guzzle), PHP 5.3+ HTTP client and framework for building RESTful web service clients;
* `SocketHttpAdapter` to use a [socket](http://www.php.net/manual/function.fsockopen.php);
* `ZendHttpAdapter` to use [Zend Http Client](http://framework.zend.com/manual/2.0/en/modules/zend.http.client.html).
* `GeoIP2Adapter` to use [GeoIP2 Database Reader](https://github.com/maxmind/GeoIP2-php#database-reader) or the [Webservice Client](https://github.com/maxmind/GeoIP2-php#web-service-client) by MaxMind.


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

### GeoIP2DatabaseProvider ###

The `GeoIP2Provider` named `maxmind_geoip2` is able to geocode **IPv4 and IPv6 addresses**
only - it makes use of the MaxMind GeoIP2 databases or the webservice.

It requires either the [database file](http://dev.maxmind.com/geoip/geoip2/geolite2/), or the [webservice](http://dev.maxmind.com/geoip/geoip2/web-services/) - represented by the GeoIP2 Provider, which is injected to the `GeoIP2Adapter`. The [geoip2/geoip2](https://packagist.org/packages/geoip2/geoip2) package must be installed.

This provider will only work with the corresponding `GeoIP2Adapter`.

**Usage:**

// Maxmind GeoIP2 Provider: e.g. the database reader
$reader = new \GeoIp2\Database\Reader('/path/to/database');

$adapter = new \Geocoder\HttpAdapter\GeoIP2Adapter($reader);
$provider = new \Geocoder\Provider\GeoIP2Provider($adapter);
$geocoder = new \Geocoder\Geocoder($provider);

$result = $geocoder->geocode('74.200.247.59');

### GeonamesProvider ###

Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@
"kriswallsmith/buzz": "@stable",
"guzzle/guzzle": "@stable",
"zendframework/zend-http": "~2.1",
"geoip/geoip": "~1.13"
"geoip/geoip": "~1.13",
"geoip2/geoip2": "~0.6"
},
"suggest": {
"kriswallsmith/buzz": "Enabling Buzz allows you to use the BuzzHttpAdapter.",
"ext-curl": "Enabling the curl extension allows you to use the CurlHttpAdapter.",
"ext-geoip": "Enabling the geoip extension allows you to use the MaxMindProvider.",
"guzzle/guzzle": "Enabling Guzzle allows you to use the GuzzleHttpAdapter.",
"zendframework/zend-http": "Enabling Zend Http allows you to use the ZendHttpAdapter.",
"geoip/geoip": "If you are going to use the MaxMindBinaryProvider (conflict with geoip extension)."
"geoip/geoip": "If you are going to use the MaxMindBinaryProvider (conflict with geoip extension).",
"geoip2/geoip2": "If you are going to use the GeoIP2DatabaseProvider."
},
"autoload": {
"psr-0": { "Geocoder": "src/" }
Expand Down
137 changes: 137 additions & 0 deletions src/Geocoder/HttpAdapter/GeoIP2Adapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

/**
* This file is part of the Geocoder package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/

namespace Geocoder\HttpAdapter;

use Geocoder\Exception\InvalidArgumentException;
use Geocoder\Exception\UnsupportedException;
use GeoIp2\ProviderInterface;

/**
* @author Jens Wiese <jens@howtrueisfalse.de>
*/
class GeoIP2Adapter implements HttpAdapterInterface
{
/**
* GeoIP2 models (e.g. city or country)
*/
const GEOIP2_MODEL_CITY = 'city';
const GEOIP2_MODEL_COUNTRY = 'country';
const GEOIP2_MODEL_OMNI = 'omni';

/**
* @var ProviderInterface
*/
protected $geoIp2Provider;

/**
* @var string
*/
protected $geoIP2Model;

/**
* @var string
*/
protected $locale;

/**
* @param \GeoIp2\ProviderInterface $geoIpProvider
* @param string $geoIP2Model (e.g. self::GEOIP2_MODEL_CITY)
* @throws \Geocoder\Exception\UnsupportedException
* @internal param string $dbFile
*/
public function __construct(ProviderInterface $geoIpProvider, $geoIP2Model = self::GEOIP2_MODEL_CITY)
{
$this->geoIp2Provider = $geoIpProvider;

if (false === $this->isSupportedGeoIP2Model($geoIP2Model)) {
throw new UnsupportedException(
sprintf('Model "%s" is not available.', $geoIP2Model)
);
}

$this->geoIP2Model = $geoIP2Model;
}

/**
* @param string $locale
* @return $this
*/
public function setLocale($locale)
{
$this->locale = $locale;

return $this;
}

/**
* @return string
*/
public function getLocale()
{
return $this->locale;
}

/**
* Returns the content fetched from a given resource.
*
* @param string $url (e.g. file://database?127.0.0.1)
* @throws \Geocoder\Exception\UnsupportedException
* @throws \Geocoder\Exception\InvalidArgumentException
* @return string
*/
public function getContent($url)
{
if (false === filter_var($url, FILTER_VALIDATE_URL)) {
throw new InvalidArgumentException(
sprintf('"%s" must be called with a valid url. Got "%s" instead.', __METHOD__, $url)
);
}

$ipAddress = parse_url($url, PHP_URL_QUERY);

if (false === filter_var($ipAddress, FILTER_VALIDATE_IP)) {
throw new InvalidArgumentException('URL must contain a valid query-string (an IP address, 127.0.0.1 for instance)');
}

$result = $this->geoIp2Provider
->{$this->geoIP2Model}($ipAddress)
->jsonSerialize();

return json_encode($result);
}

/**
* Returns the name of the Adapter.
*
* @return string
*/
public function getName()
{
return 'maxmind_geoip2';
}

/**
* Returns whether method is supported by GeoIP2
*
* @param string $method
* @return bool
*/
protected function isSupportedGeoIP2Model($method)
{
$availableMethods = array(
self::GEOIP2_MODEL_CITY,
self::GEOIP2_MODEL_COUNTRY,
self::GEOIP2_MODEL_OMNI
);

return in_array($method, $availableMethods);
}
}
100 changes: 100 additions & 0 deletions src/Geocoder/Provider/GeoIP2Provider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

/**
* This file is part of the Geocoder package.
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* @license MIT License
*/

namespace Geocoder\Provider;

use Geocoder\Exception\NoResultException;
use Geocoder\Exception\InvalidArgumentException;
use Geocoder\Exception\UnsupportedException;
use Geocoder\HttpAdapter\GeoIP2Adapter;
use Geocoder\HttpAdapter\HttpAdapterInterface;
use GeoIp2\Exception\AddressNotFoundException;
use GeoIp2\Model\City;

/**
* @author Jens Wiese <jens@howtrueisfalse.de>
*/
class GeoIP2Provider extends AbstractProvider implements ProviderInterface
{
/**
* {@inheritdoc}
*/
public function __construct(HttpAdapterInterface $adapter, $locale = 'en')
{
if (false === $adapter instanceof GeoIP2Adapter) {
throw new InvalidArgumentException(
'GeoIP2Adapter is needed in order to access the GeoIP2 service.'
);
}

parent::__construct($adapter, $locale);
}

/**
* {@inheritDoc}
*/
public function getGeocodedData($address)
{
if (false === filter_var($address, FILTER_VALIDATE_IP)) {
throw new UnsupportedException(sprintf('The %s does not support street addresses.', __CLASS__));
}

if ('127.0.0.1' === $address) {
return $this->getLocalhostDefaults();
}

$result = json_decode($this->executeQuery($address));

return array($this->fixEncoding(array_merge($this->getDefaults(), array(
'countryCode' => (isset($result->country->iso_code) ? $result->country->iso_code : null),
'country' => (isset($result->country->names->{$this->locale}) ? $result->country->names->{$this->locale} : null),
'city' => (isset($result->city->names->{$this->locale}) ? $result->city->names->{$this->locale} : null),
'latitude' => (isset($result->location->latitude) ? $result->location->latitude : null),
'longitude' => (isset($result->location->longitude) ? $result->location->longitude : null),
'timezone' => (isset($result->location->timezone) ? $result->location->timezone : null),
'zipcode' => (isset($result->location->postalcode) ? $result->location->postalcode : null),
))));
}

/**
* {@inheritDoc}
*/
public function getReversedData(array $coordinates)
{
throw new UnsupportedException(sprintf('The %s is not able to do reverse geocoding.', __CLASS__));
}

/**
* {@inheritDoc}
*/
public function getName()
{
return 'maxmind_geoip2';
}

/**
* @param string $address
* @throws \Geocoder\Exception\NoResultException
* @return City
*/
protected function executeQuery($address)
{
$uri = sprintf('file://geoip?%s', $address);

try {
$result = $this->getAdapter()->setLocale($this->locale)->getContent($uri);
} catch (AddressNotFoundException $e) {
throw new NoResultException(sprintf('No results found for IP address %s', $address));
}

return $result;
}

}
Loading

0 comments on commit 12b862d

Please sign in to comment.