Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement MaxData GeoIP2 provider and database adapter #303

Merged
merged 3 commits into from
Jun 13, 2014
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 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).
* `GeoIP2DatabaseAdapter` to use [GeoIP2 Database Reader by MaxMind](https://github.com/maxmind/GeoIP2-php#database-reader).


### 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 database file](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 @@ -259,6 +261,22 @@ 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 `GeoIP2DatabaseProvider` named `geoip2_database` is able to geocode **IPv4 and IPv6 addresses**
only - it makes use of the MaxMind GeoIP2 databases.

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.

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

**Usage:**

$adapter = new \Geocoder\HttpAdapter\GeoIP2DatabaseAdapter('/path/to/database');
$provider = new \Geocoder\Provider\GeoIP2DatabaseProvider($adapter);
$geocoder = new \Geocoder\Geocoder($provider);

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

### GeonamesProvider ###

Expand Down
7 changes: 5 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@
"kriswallsmith/buzz": "@stable",
"guzzle/guzzle": "@stable",
"zendframework/zend-http": "~2.1",
"geoip/geoip": "~1.13"
"geoip/geoip": "~1.13",
"geoip2/geoip2": "~0.6",
"mikey179/vfsStream": "v1.2.0"
},
"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
168 changes: 168 additions & 0 deletions src/Geocoder/HttpAdapter/GeoIP2DatabaseAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?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\RuntimeException;
use Geocoder\Exception\InvalidArgumentException;
use Geocoder\Exception\UnsupportedException;
use GeoIp2\Database\Reader;

/**
* @author Jens Wiese <jens@howtrueisfalse.de>
*/
class GeoIP2DatabaseAdapter implements HttpAdapterInterface
{
/**
* Database file types
*/
const GEOIP2_CITY = 'geoip2_city';
const GEOIP2_COUNTRY = 'geoip2_country';

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

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

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

/**
* @var Reader
*/
protected $dbReader;

/**
* @param string $dbFile
* @param string $dbType (e.g. self::GEOIP2_CITY)
* @throws \Geocoder\Exception\RuntimeException
* @throws \Geocoder\Exception\InvalidArgumentException
*/
public function __construct($dbFile, $dbType = self::GEOIP2_CITY)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not inject a \GeoIp2\Database\Reader instance? This would reduce the complexity, made it more extensible and it will be easier to write a test (remove the vfsStream).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's true. I decided to hide the dependency to the GeoIP2 library in order to ease up the handling.
The adapter just needs the path to the file - everything else is handled by the Adapter.

Hmm!? Don't know ...?

Thank you for your feedback! Much appreciated! :)

Jens.

{
if (false === class_exists('\GeoIp2\Database\Reader')) {
throw new RuntimeException(sprintf("The %s requires maxmind's lib 'geoip2/geoip2'", __CLASS__));
}

if (false === is_file($dbFile)) {
throw new InvalidArgumentException(sprintf('Given MaxMind database file "%s" is not a file.', $dbFile));
}

if (false === is_readable($dbFile)) {
throw new InvalidArgumentException(sprintf('Given MaxMind database file "%s" is not readable.', $dbFile));
}

$this->dbFile = $dbFile;
$this->dbType = $dbType;
}

/**
* @param Reader $dbReader
*/
public function setDbReader(Reader $dbReader)
{
$this->dbReader = $dbReader;
}

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

return $this;
}

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

/**
* Destruct (e.g. database reader)
*/
public function __destruct()
{
$this->getDbReader()->close();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really safe? I think php may throws an exception with a "An error occured in Unknown 0" message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I'm not sure. During unit testing nothing bad happened. (?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

/**
* 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)
{
$url = trim($url);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, not necessarily. I'll remove it. ;)


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 (a IP address, 127.0.0.1 for instance)');
}

switch ($this->dbType) {
case self::GEOIP2_CITY:
$result = $this->getDbReader()->city($ipAddress)->jsonSerialize();
break;
default:
throw new UnsupportedException(
sprintf('Database type "%s" not implemented yet.', $this->dbType)
);
}

return json_encode($result);
}

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

/**
* Returns database reader
*
* @return Reader
*/
protected function getDbReader()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the dependency injection - you can safely remove this method.

{
if (is_null($this->dbReader)) {
$this->dbReader = new Reader($this->dbFile, $this->getLocale());
}

return $this->dbReader;
}
}
108 changes: 108 additions & 0 deletions src/Geocoder/Provider/GeoIP2DatabaseProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?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\RuntimeException;
use Geocoder\Exception\UnsupportedException;
use Geocoder\HttpAdapter\AdapterInterface;
use Geocoder\HttpAdapter\GeoIP2DatabaseAdapter;
use Geocoder\HttpAdapter\HttpAdapterInterface;
use GeoIp2\Database\Reader;
use GeoIp2\Exception\AddressNotFoundException;
use GeoIp2\Model\City;

/**
* @author Jens Wiese <jens@howtrueisfalse.de>
*/
class GeoIP2DatabaseProvider extends AbstractProvider implements ProviderInterface
{
/**
* @var string
*/
protected $dbFile;

/**
* {@inheritdoc}
*/
public function __construct(HttpAdapterInterface $adapter, $locale = 'en')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not change the interface to GeoIP2DatabaseAdapter and remove the instanceof call?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!
This is exactly what I was struggling with in #302 . The main problem is that the AbstractProvider defines the constructor signature. Thus I was about to propose to change the typehint to something like AdapterInterface in order to have the ability to use a different adapter. But that would mean that we have to adapt all the HttpAdapters, as well as the directory structure/namespaces to unify that.

But as @willdurand mentioned that would break the library's philosophy.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep BC and improve the adapter for the next major release.

{
if (false === $adapter instanceof GeoIP2DatabaseAdapter) {
throw new InvalidArgumentException(
'GeoIP2DatabaseAdapter is needed in order to access the GeoIP2 databases.'
);
}

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 'geoip2_database';
}

/**
* @param string $address
* @throws \Geocoder\Exception\NoResultException
* @return City
*/
protected function executeQuery($address)
{
$uri = sprintf('file://database?%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