Completed
Push — master ( 0b5b3b...978625 )
by Tobias
15:52 queued 11:44
created

GoogleMaps::buildQuery()   C

Complexity

Conditions 7
Paths 40

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 14
cts 14
cp 1
rs 6.7272
c 0
b 0
f 0
cc 7
eloc 14
nc 40
nop 3
crap 7
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Geocoder package.
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @license    MIT License
11
 */
12
13
namespace Geocoder\Provider\GoogleMaps;
14
15
use Geocoder\Collection;
16
use Geocoder\Exception\InvalidCredentials;
17
use Geocoder\Exception\InvalidServerResponse;
18
use Geocoder\Exception\QuotaExceeded;
19
use Geocoder\Exception\UnsupportedOperation;
20
use Geocoder\Model\AddressCollection;
21
use Geocoder\Model\AddressBuilder;
22
use Geocoder\Query\GeocodeQuery;
23
use Geocoder\Query\ReverseQuery;
24
use Geocoder\Http\Provider\AbstractHttpProvider;
25
use Geocoder\Provider\GoogleMaps\Model\GoogleAddress;
26
use Geocoder\Provider\Provider;
27
use Http\Client\HttpClient;
28
29
/**
30
 * @author William Durand <[email protected]>
31
 */
32
final class GoogleMaps extends AbstractHttpProvider implements Provider
33
{
34
    /**
35
     * @var string
36
     */
37
    const GEOCODE_ENDPOINT_URL_SSL = 'https://maps.googleapis.com/maps/api/geocode/json?address=%s';
38
39
    /**
40
     * @var string
41
     */
42
    const REVERSE_ENDPOINT_URL_SSL = 'https://maps.googleapis.com/maps/api/geocode/json?latlng=%F,%F';
43
44
    /**
45
     * @var string|null
46
     */
47
    private $region;
48
49
    /**
50
     * @var string|null
51
     */
52
    private $apiKey;
53
54
    /**
55
     * @var string
56
     */
57
    private $clientId;
58
59
    /**
60
     * @var string|null
61
     */
62
    private $privateKey;
63
64
    /**
65
     * @var string|null
66
     */
67
    private $channel;
68
69
    /**
70
     * Google Maps for Business
71
     * https://developers.google.com/maps/documentation/business/.
72
     *
73
     * @param HttpClient $client     An HTTP adapter
74
     * @param string     $clientId   Your Client ID
75
     * @param string     $privateKey Your Private Key (optional)
76
     * @param string     $region     Region biasing (optional)
77
     * @param string     $apiKey     Google Geocoding API key (optional)
78
     * @param string     $channel    Google Channel parameter (optional)
79
     *
80
     * @return GoogleMaps
81
     */
82 5
    public static function business(HttpClient $client, string $clientId, string $privateKey = null, string $region = null, string $apiKey = null, string $channel = null)
83
    {
84 5
        $provider = new self($client, $region, $apiKey);
85 5
        $provider->clientId = $clientId;
86 5
        $provider->privateKey = $privateKey;
87 5
        $provider->channel = $channel;
88
89 5
        return $provider;
90
    }
91
92
    /**
93
     * @param HttpClient $client An HTTP adapter
94
     * @param string     $region Region biasing (optional)
95
     * @param string     $apiKey Google Geocoding API key (optional)
96
     */
97 41
    public function __construct(HttpClient $client, string $region = null, string $apiKey = null)
98
    {
99 41
        parent::__construct($client);
100
101 41
        $this->region = $region;
102 41
        $this->apiKey = $apiKey;
103 41
    }
104
105 29
    public function geocodeQuery(GeocodeQuery $query): Collection
106
    {
107
        // Google API returns invalid data if IP address given
108
        // This API doesn't handle IPs
109 29
        if (filter_var($query->getText(), FILTER_VALIDATE_IP)) {
110 3
            throw new UnsupportedOperation('The GoogleMaps provider does not support IP addresses, only street addresses.');
111
        }
112
113 26
        $url = sprintf(self::GEOCODE_ENDPOINT_URL_SSL, rawurlencode($query->getText()));
114 26
        if (null !== $bounds = $query->getBounds()) {
115
            $url .= sprintf('&bounds=%s,%s|%s,%s', $bounds->getSouth(), $bounds->getWest(), $bounds->getNorth(), $bounds->getEast());
116
        }
117
118 26
        return $this->fetchUrl($url, $query->getLocale(), $query->getLimit(), $query->getData('region', $this->region));
119
    }
120
121 11
    public function reverseQuery(ReverseQuery $query): Collection
122
    {
123 11
        $coordinate = $query->getCoordinates();
124 11
        $url = sprintf(self::REVERSE_ENDPOINT_URL_SSL, $coordinate->getLatitude(), $coordinate->getLongitude());
125
126 11
        if (null !== $locationType = $query->getData('location_type')) {
127
            $url .= '&location_type='.urlencode($locationType);
128
        }
129
130 11
        if (null !== $resultType = $query->getData('result_type')) {
131
            $url .= '&result_type='.urlencode($resultType);
132
        }
133
134 11
        return $this->fetchUrl($url, $query->getLocale(), $query->getLimit(), $query->getData('region', $this->region));
135
    }
136
137
    /**
138
     * {@inheritdoc}
139
     */
140 17
    public function getName(): string
141
    {
142 17
        return 'google_maps';
143
    }
144
145
    /**
146
     * @param string $url
147
     * @param string $locale
148
     *
149
     * @return string query with extra params
150
     */
151 37
    private function buildQuery(string $url, string $locale = null, string $region = null): string
152
    {
153 37
        if (null !== $locale) {
154 4
            $url = sprintf('%s&language=%s', $url, $locale);
155
        }
156
157 37
        if (null !== $region) {
158 1
            $url = sprintf('%s&region=%s', $url, $region);
159
        }
160
161 37
        if (null !== $this->apiKey) {
162 2
            $url = sprintf('%s&key=%s', $url, $this->apiKey);
163
        }
164
165 37
        if (null !== $this->clientId) {
166 5
            $url = sprintf('%s&client=%s', $url, $this->clientId);
167
168 5
            if (null !== $this->channel) {
169 1
                $url = sprintf('%s&channel=%s', $url, $this->channel);
170
            }
171
172 5
            if (null !== $this->privateKey) {
173 4
                $url = $this->signQuery($url);
174
            }
175
        }
176
177 37
        return $url;
178
    }
179
180
    /**
181
     * @param string $url
182
     * @param string $locale
183
     * @param int    $limit
184
     * @param string $region
185
     *
186
     * @return AddressCollection
187
     *
188
     * @throws InvalidServerResponse
189
     * @throws InvalidCredentials
190
     */
191 37
    private function fetchUrl(string $url, string $locale = null, int $limit, string $region = null): AddressCollection
192
    {
193 37
        $url = $this->buildQuery($url, $locale, $region);
194 37
        $content = $this->getUrlContents($url);
195 23
        $json = $this->validateResponse($url, $content);
196
197
        // no result
198 18
        if (!isset($json->results) || !count($json->results) || 'OK' !== $json->status) {
199 2
            return new AddressCollection([]);
200
        }
201
202 16
        $results = [];
203 16
        foreach ($json->results as $result) {
204 16
            $builder = new AddressBuilder($this->getName());
205 16
            $this->parseCoordinates($builder, $result);
206
207
            // set official Google place id
208 16
            if (isset($result->place_id)) {
209 15
                $builder->setValue('id', $result->place_id);
210
            }
211
212
            // update address components
213 16
            foreach ($result->address_components as $component) {
214 16
                foreach ($component->types as $type) {
215 16
                    $this->updateAddressComponent($builder, $type, $component);
216
                }
217
            }
218
219
            /** @var GoogleAddress $address */
220 16
            $address = $builder->build(GoogleAddress::class);
221 16
            $address = $address->withId($builder->getValue('id'));
222 16
            if (isset($result->geometry->location_type)) {
223 16
                $address = $address->withLocationType($result->geometry->location_type);
224
            }
225 16
            if (isset($result->types)) {
226 16
                $address = $address->withResultType($result->types);
227
            }
228 16
            if (isset($result->formatted_address)) {
229 16
                $address = $address->withFormattedAddress($result->formatted_address);
230
            }
231 16
            $address = $address->withStreetAddress($builder->getValue('street_address'));
232 16
            $address = $address->withIntersection($builder->getValue('intersection'));
233 16
            $address = $address->withPolitical($builder->getValue('political'));
234 16
            $address = $address->withColloquialArea($builder->getValue('colloquial_area'));
235 16
            $address = $address->withWard($builder->getValue('ward'));
236 16
            $address = $address->withNeighborhood($builder->getValue('neighborhood'));
237 16
            $address = $address->withPremise($builder->getValue('premise'));
238 16
            $address = $address->withSubpremise($builder->getValue('subpremise'));
239 16
            $address = $address->withNaturalFeature($builder->getValue('natural_feature'));
240 16
            $address = $address->withAirport($builder->getValue('airport'));
241 16
            $address = $address->withPark($builder->getValue('park'));
242 16
            $address = $address->withPointOfInterest($builder->getValue('point_of_interest'));
243 16
            $address = $address->withEstablishment($builder->getValue('establishment'));
244 16
            $address = $address->withSubLocalityLevels($builder->getValue('subLocalityLevel', []));
245 16
            $results[] = $address;
246
247 16
            if (count($results) >= $limit) {
248 16
                break;
249
            }
250
        }
251
252 16
        return new AddressCollection($results);
253
    }
254
255
    /**
256
     * Update current resultSet with given key/value.
257
     *
258
     * @param AddressBuilder $builder
259
     * @param string         $type    Component type
260
     * @param object         $values  The component values
261
     */
262 16
    private function updateAddressComponent(AddressBuilder $builder, string $type, $values)
263
    {
264
        switch ($type) {
265 16
            case 'postal_code':
266 14
                $builder->setPostalCode($values->long_name);
267
268 14
                break;
269
270 16
            case 'locality':
271 16
            case 'postal_town':
272 16
                $builder->setLocality($values->long_name);
273
274 16
                break;
275
276 16
            case 'administrative_area_level_1':
277 16
            case 'administrative_area_level_2':
278 16
            case 'administrative_area_level_3':
279 16
            case 'administrative_area_level_4':
280 16
            case 'administrative_area_level_5':
281 15
                $builder->addAdminLevel(intval(substr($type, -1)), $values->long_name, $values->short_name);
282
283 15
                break;
284
285 16
            case 'sublocality_level_1':
286 16
            case 'sublocality_level_2':
287 16
            case 'sublocality_level_3':
288 16
            case 'sublocality_level_4':
289 16
            case 'sublocality_level_5':
290 6
                $subLocalityLevel = $builder->getValue('subLocalityLevel', []);
291 6
                $subLocalityLevel[] = [
292 6
                    'level' => intval(substr($type, -1)),
293 6
                    'name' => $values->long_name,
294 6
                    'code' => $values->short_name,
295
                ];
296 6
                $builder->setValue('subLocalityLevel', $subLocalityLevel);
297
298 6
                break;
299
300 16
            case 'country':
301 16
                $builder->setCountry($values->long_name);
302 16
                $builder->setCountryCode($values->short_name);
303
304 16
                break;
305
306 16
            case 'street_number':
307 7
                $builder->setStreetNumber($values->long_name);
308
309 7
                break;
310
311 16
            case 'route':
312 8
                $builder->setStreetName($values->long_name);
313
314 8
                break;
315
316 16
            case 'sublocality':
317 6
                $builder->setSubLocality($values->long_name);
318
319 6
                break;
320
321 16
            case 'street_address':
322 16
            case 'intersection':
323 16
            case 'political':
324 13
            case 'colloquial_area':
325 11
            case 'ward':
326 11
            case 'neighborhood':
327 9
            case 'premise':
328 7
            case 'subpremise':
329 6
            case 'natural_feature':
330 6
            case 'airport':
331 6
            case 'park':
332 6
            case 'point_of_interest':
333 6
            case 'establishment':
334 16
                $builder->setValue($type, $values->long_name);
335
336 16
                break;
337
338
            default:
339
        }
340 16
    }
341
342
    /**
343
     * Sign a URL with a given crypto key
344
     * Note that this URL must be properly URL-encoded
345
     * src: http://gmaps-samples.googlecode.com/svn/trunk/urlsigning/UrlSigner.php-source.
346
     *
347
     * @param string $query Query to be signed
348
     *
349
     * @return string $query query with signature appended
350
     */
351 4
    private function signQuery(string $query): string
352
    {
353 4
        $url = parse_url($query);
354
355 4
        $urlPartToSign = $url['path'].'?'.$url['query'];
356
357
        // Decode the private key into its binary format
358 4
        $decodedKey = base64_decode(str_replace(['-', '_'], ['+', '/'], $this->privateKey));
359
360
        // Create a signature using the private key and the URL-encoded
361
        // string using HMAC SHA1. This signature will be binary.
362 4
        $signature = hash_hmac('sha1', $urlPartToSign, $decodedKey, true);
363
364 4
        $encodedSignature = str_replace(['+', '/'], ['-', '_'], base64_encode($signature));
365
366 4
        return sprintf('%s&signature=%s', $query, $encodedSignature);
367
    }
368
369
    /**
370
     * Decode the response content and validate it to make sure it does not have any errors.
371
     *
372
     * @param string $url
373
     * @param string $content
374
     *
375
     * @return mixed result form json_decode()
376
     *
377
     * @throws InvalidCredentials
378
     * @throws InvalidServerResponse
379
     * @throws QuotaExceeded
380
     */
381 23
    private function validateResponse(string $url, $content)
382
    {
383
        // Throw exception if invalid clientID and/or privateKey used with GoogleMapsBusinessProvider
384 23
        if (false !== strpos($content, "Provided 'signature' is not valid for the provided client ID")) {
385 2
            throw new InvalidCredentials(sprintf('Invalid client ID / API Key %s', $url));
386
        }
387
388 21
        $json = json_decode($content);
389
390
        // API error
391 21
        if (!isset($json)) {
392
            throw InvalidServerResponse::create($url);
393
        }
394
395 21
        if ('REQUEST_DENIED' === $json->status && 'The provided API key is invalid.' === $json->error_message) {
396 2
            throw new InvalidCredentials(sprintf('API key is invalid %s', $url));
397
        }
398
399 19
        if ('REQUEST_DENIED' === $json->status) {
400
            throw new InvalidServerResponse(
401
                sprintf('API access denied. Request: %s - Message: %s', $url, $json->error_message)
402
            );
403
        }
404
405
        // you are over your quota
406 19
        if ('OVER_QUERY_LIMIT' === $json->status) {
407 1
            throw new QuotaExceeded(sprintf('Daily quota exceeded %s', $url));
408
        }
409
410 18
        return $json;
411
    }
412
413
    /**
414
     * Parse coordinats and bounds.
415
     *
416
     * @param AddressBuilder $builder
417
     * @param $result
418
     */
419 16
    private function parseCoordinates(AddressBuilder $builder, $result)
420
    {
421 16
        $coordinates = $result->geometry->location;
422 16
        $builder->setCoordinates($coordinates->lat, $coordinates->lng);
423
424 16
        if (isset($result->geometry->bounds)) {
425 9
            $builder->setBounds(
426 9
                $result->geometry->bounds->southwest->lat,
427 9
                $result->geometry->bounds->southwest->lng,
428 9
                $result->geometry->bounds->northeast->lat,
429 9
                $result->geometry->bounds->northeast->lng
430
            );
431 11
        } elseif (isset($result->geometry->viewport)) {
432 11
            $builder->setBounds(
433 11
                $result->geometry->viewport->southwest->lat,
434 11
                $result->geometry->viewport->southwest->lng,
435 11
                $result->geometry->viewport->northeast->lat,
436 11
                $result->geometry->viewport->northeast->lng
437
            );
438
        } elseif ('ROOFTOP' === $result->geometry->location_type) {
439
            // Fake bounds
440
            $builder->setBounds(
441
                $coordinates->lat,
442
                $coordinates->lng,
443
                $coordinates->lat,
444
                $coordinates->lng
445
            );
446
        }
447 16
    }
448
}
449