diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9d7ca88 --- /dev/null +++ b/composer.json @@ -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" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..eb9a234 --- /dev/null +++ b/composer.lock @@ -0,0 +1,67 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "926f6e9e07588507189a2fd015a05f60", + "packages": [ + { + "name": "psr/simple-cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "time": "2017-10-23T01:57:42+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "1.1.0" +} diff --git a/src/Perfacilis/Geocoder/Geocoder.php b/src/Perfacilis/Geocoder/Geocoder.php new file mode 100644 index 0000000..86f728e --- /dev/null +++ b/src/Perfacilis/Geocoder/Geocoder.php @@ -0,0 +1,153 @@ + + * @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); + } +} diff --git a/src/Perfacilis/Geocoder/Query.php b/src/Perfacilis/Geocoder/Query.php new file mode 100644 index 0000000..d4b20d4 --- /dev/null +++ b/src/Perfacilis/Geocoder/Query.php @@ -0,0 +1,36 @@ + + * @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 = []; +} diff --git a/src/Perfacilis/Geocoder/Result.php b/src/Perfacilis/Geocoder/Result.php new file mode 100644 index 0000000..04daee5 --- /dev/null +++ b/src/Perfacilis/Geocoder/Result.php @@ -0,0 +1,109 @@ + + * @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 + )); + } +}