-
Notifications
You must be signed in to change notification settings - Fork 517
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
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
{ | ||
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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually I'm not sure. During unit testing nothing bad happened. (?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's a problem: https://github.com/maxmind/MaxMind-DB-Reader-php/blob/master/src/MaxMind/Db/Reader.php#L286 |
||
} | ||
|
||
/** | ||
* 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't needed. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} |
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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not change the interface to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point! But as @willdurand mentioned that would break the library's philosophy. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
|
||
} |
There was a problem hiding this comment.
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).There was a problem hiding this comment.
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.