Skip to content

Commit

Permalink
Merge pull request #1 from perfacilis/feature/first-magic-code
Browse files Browse the repository at this point in the history
Feature: Composer defs; Geocoder classess, this is all we need.
  • Loading branch information
royarisse authored Jun 15, 2021
2 parents 4a98824 + 66f10e0 commit 64908cf
Show file tree
Hide file tree
Showing 6 changed files with 387 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/vendor/
21 changes: 21 additions & 0 deletions composer.json
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"
}
}
}
67 changes: 67 additions & 0 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

153 changes: 153 additions & 0 deletions src/Perfacilis/Geocoder/Geocoder.php
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);
}
}
36 changes: 36 additions & 0 deletions src/Perfacilis/Geocoder/Query.php
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 = [];
}
109 changes: 109 additions & 0 deletions src/Perfacilis/Geocoder/Result.php
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
));
}
}

0 comments on commit 64908cf

Please sign in to comment.