Completed
Push — master ( 5d031b...aa4864 )
by Tobias
03:51
created

GoogleMaps::updateAddressComponent()   D

Complexity

Conditions 31
Paths 31

Size

Total Lines 79
Code Lines 57

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 56
CRAP Score 31

Importance

Changes 0
Metric Value
dl 0
loc 79
ccs 56
cts 56
cp 1
rs 4.9882
c 0
b 0
f 0
cc 31
eloc 57
nc 31
nop 3
crap 31

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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