-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from perfacilis/feature/first-magic-code
Feature: Composer defs; Geocoder classess, this is all we need.
- Loading branch information
Showing
6 changed files
with
387 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/vendor/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
{ | ||
"name": "perfacilis/geocoder", | ||
"description": "Simple Geocoder with Cache using Google Maps API", | ||
"type": "library", | ||
"license": "MIT", | ||
"authors": [ | ||
{ | ||
"name": "Roy Arisse", | ||
"email": "roy@perfacilis.com", | ||
"homepage": "https://perfacilis.com" | ||
} | ||
], | ||
"require": { | ||
"psr/simple-cache": "^1.0" | ||
}, | ||
"autoload": { | ||
"psr-4": { | ||
"Perfacilis\\": "src/Perfacilis" | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
<?php | ||
|
||
namespace Perfacilis\Geocoder; | ||
|
||
use Exception; | ||
use Psr\SimpleCache\CacheInterface; | ||
|
||
/** | ||
* @author Roy Arisse <support@perfacilis.com> | ||
* @copyright (c) 2021, Perfacilis | ||
*/ | ||
class Geocoder | ||
{ | ||
public function __construct(string $api_key = '') | ||
{ | ||
$this->api_key = $api_key; | ||
} | ||
|
||
/** | ||
* Set cache interface to save queries, thus save a bit of moneyz. | ||
* | ||
* @param CacheInterface $cacher | ||
* @param int $ttl Results lifetime in seconds | ||
* @return void | ||
*/ | ||
public function setCacheInterface(CacheInterface $cacher, int $ttl = 0): void | ||
{ | ||
$this->cacher = $cacher; | ||
$this->cache_ttl = $ttl; | ||
} | ||
|
||
public function geocode(string $address): Result | ||
{ | ||
$params = Query::fromAddress($address)->getParamms(); | ||
return $this->getResult($params); | ||
} | ||
|
||
public function reverseGeocode(float $lat, float $lng): Result | ||
{ | ||
$params = Query::fromLatLng($lat, $lng)->getParamms(); | ||
return $this->getResult($params); | ||
} | ||
|
||
private const ENDPOINT = 'https://maps.googleapis.com/maps/api/geocode/json'; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $api_key = ''; | ||
|
||
/** | ||
* @var CacheInterface | ||
*/ | ||
private $cacher = null; | ||
|
||
/** | ||
* @var int | ||
*/ | ||
private $cache_ttl = 0; | ||
|
||
private function getResult(array $params): Result | ||
{ | ||
$items = $this->query($params); | ||
return $items[0]; | ||
} | ||
|
||
/** | ||
* @param array $params | ||
* @return Result[] | ||
* @throws Exception | ||
*/ | ||
private function query(array $params): array | ||
{ | ||
$result = $this->queryCache($params); | ||
if (!$result) { | ||
$result = $this->queryEndpoint($params); | ||
$this->saveCache($params, $result); | ||
} | ||
|
||
$items = []; | ||
foreach ($result['results'] as $item) { | ||
$items[] = new Result($item); | ||
} | ||
|
||
return $items; | ||
} | ||
|
||
private function queryEndpoint(array $params): array | ||
{ | ||
// Append api key | ||
$params['key'] = $this->api_key; | ||
|
||
// Build URL with query parameters | ||
$url = self::ENDPOINT . '?' . http_build_query($params); | ||
|
||
$ch = curl_init(); | ||
curl_setopt_array($ch, [ | ||
CURLOPT_URL => $url, | ||
CURLOPT_TIMEOUT => 10, | ||
CURLOPT_RETURNTRANSFER => true | ||
]); | ||
|
||
$json = curl_exec($ch); | ||
$result = $json ? json_decode($json, true) : []; | ||
if (!$result) { | ||
throw new Exception(sprintf('Invalid json response from %s: %s', $url, $json)); | ||
} | ||
|
||
$error = curl_error($ch); | ||
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE); | ||
if ($error || $status < 200 || $status > 299) { | ||
throw new Exception(sprintf('Invalid status %d from %s; error: %s.', $status, $url, $error)); | ||
} | ||
|
||
if (array_key_exists('error_message', $result)) { | ||
throw new Exception(sprintf('Geocoder api returned error: %s', $result['error_message'])); | ||
} | ||
|
||
return $result; | ||
} | ||
|
||
/** | ||
* Get result from cache, if cacher is set | ||
* | ||
* @param array $params | ||
* @return array | ||
*/ | ||
private function queryCache(array $params): array | ||
{ | ||
if (!$this->cacher) { | ||
return []; | ||
} | ||
|
||
$key = $this->getCacheKey($params); | ||
|
||
return $this->cacher->get($key, []); | ||
} | ||
|
||
private function saveCache(array $params, array $result): void | ||
{ | ||
if (!$this->cacher) { | ||
return; | ||
} | ||
|
||
$key = $this->getCacheKey($params); | ||
$this->cacher->set($key, $result, $this->cache_ttl); | ||
} | ||
|
||
private function getCacheKey(array $params): string | ||
{ | ||
return get_called_class() . ':' . json_encode($params); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
<?php | ||
|
||
namespace Perfacilis\Geocoder; | ||
|
||
/** | ||
* @author Roy Arisse <support@perfacilis.com> | ||
* @copyright (c) 2021, Perfacilis | ||
*/ | ||
class Query | ||
{ | ||
public static function fromAddress(string $address): self | ||
{ | ||
return new self([ | ||
'address' => $address | ||
]); | ||
} | ||
|
||
public static function fromLatLng(float $lat, float $lng): self | ||
{ | ||
return new self([ | ||
'latlng' => $lat . ',' . $lng | ||
]); | ||
} | ||
|
||
public function __construct(array $params) | ||
{ | ||
$this->params = $params; | ||
} | ||
|
||
public function getParamms(): array | ||
{ | ||
return $this->params; | ||
} | ||
|
||
private $params = []; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
<?php | ||
|
||
namespace Perfacilis\Geocoder; | ||
|
||
use Exception; | ||
use InvalidArgumentException; | ||
|
||
/** | ||
* @author Roy Arisse <support@perfacilis.com> | ||
* @copyright (c) 2021, Perfacilis | ||
*/ | ||
class Result | ||
{ | ||
public function __construct(array $attr) | ||
{ | ||
$this->attr = $attr; | ||
} | ||
|
||
/** | ||
* Alias for getRoute | ||
*/ | ||
public function getStreet(): string | ||
{ | ||
return $this->getRoute(); | ||
} | ||
|
||
public function getStreetNumber(): string | ||
{ | ||
return $this->getAddressComponent('street_number'); | ||
} | ||
|
||
public function getRoute() | ||
{ | ||
return $this->getAddressComponent('route'); | ||
} | ||
|
||
public function getLocality() | ||
{ | ||
return $this->getAddressComponent('locality'); | ||
} | ||
|
||
public function getCounty() | ||
{ | ||
return $this->getAddressComponent('administrative_area_level_2'); | ||
} | ||
|
||
public function getState() | ||
{ | ||
return $this->getAddressComponent('administrative_area_level_1'); | ||
} | ||
|
||
public function getCountry() | ||
{ | ||
return $this->getAddressComponent('country'); | ||
} | ||
|
||
public function getPostalCode() | ||
{ | ||
return $this->getAddressComponent('postal_code'); | ||
} | ||
|
||
public function getFormattedAddress() | ||
{ | ||
return $this->attr['formatted_address']; | ||
} | ||
|
||
public function getPlaceId() | ||
{ | ||
return $this->attr['place_id']; | ||
} | ||
|
||
public function getLat() | ||
{ | ||
return $this->attr['geometry']['location']['lat']; | ||
} | ||
|
||
public function getLng() | ||
{ | ||
return $this->attr['geometry']['location']['lng']; | ||
} | ||
|
||
private $attr = []; | ||
|
||
private function getAddressComponent(string $key): string | ||
{ | ||
foreach ($this->attr['address_components'] as $c) { | ||
if (in_array($key, $c['types'])) { | ||
return $c['long_name']; | ||
} | ||
} | ||
|
||
throw new InvalidArgumentException(sprintf( | ||
'Given component \'%s\' doesn\'t exist.', | ||
$key | ||
)); | ||
} | ||
|
||
public function __get($name) | ||
{ | ||
if (isset($this->attr[$name])) { | ||
return $this->attr[$name]; | ||
} | ||
|
||
throw new Exception(sprintf( | ||
'Given propertly \'%s\' doesn\'t exist.', | ||
$name | ||
)); | ||
} | ||
} |