GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 691675...503bab )
by Tobias
07:29 queued 04:40
created

GoogleMaps::updateAddressComponent()   D

Complexity

Conditions 31
Paths 31

Size

Total Lines 79
Code Lines 57

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 79
rs 4.9882
cc 31
eloc 57
nc 31
nop 3

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)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $privateKey not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
71
     * @param string     $region     Region biasing (optional)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $region not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
72
     * @param string     $apiKey     Google Geocoding API key (optional)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $apiKey not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
73
     *
74
     * @return GoogleMaps
75
     */
76
    public static function business(HttpClient $client, string  $clientId, string $privateKey = null, string $region = null, string $apiKey = null)
77
    {
78
        $provider = new self($client, $region, $apiKey);
79
        $provider->clientId = $clientId;
80
        $provider->privateKey = $privateKey;
81
82
        return $provider;
83
    }
84
85
    /**
86
     * @param HttpClient $client An HTTP adapter
87
     * @param string     $region Region biasing (optional)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $region not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
88
     * @param string     $apiKey Google Geocoding API key (optional)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $apiKey not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
89
     */
90
    public function __construct(HttpClient $client, string $region = null, string $apiKey = null)
91
    {
92
        parent::__construct($client);
93
94
        $this->region = $region;
95
        $this->apiKey = $apiKey;
96
    }
97
98
    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
        if (filter_var($query->getText(), FILTER_VALIDATE_IP)) {
103
            throw new UnsupportedOperation('The GoogleMaps provider does not support IP addresses, only street addresses.');
104
        }
105
106
        $url = sprintf(self::GEOCODE_ENDPOINT_URL_SSL, rawurlencode($query->getText()));
107
        if (null !== $bounds = $query->getBounds()) {
108
            $url .= sprintf('&bounds=%s,%s|%s,%s', $bounds->getSouth(), $bounds->getWest(), $bounds->getNorth(), $bounds->getEast());
109
        }
110
111
        return $this->fetchUrl($url, $query->getLocale(), $query->getLimit(), $query->getData('region', $this->region));
112
    }
113
114
    public function reverseQuery(ReverseQuery $query): Collection
115
    {
116
        $coordinate = $query->getCoordinates();
117
        $url = sprintf(self::REVERSE_ENDPOINT_URL_SSL, $coordinate->getLatitude(), $coordinate->getLongitude());
118
119
        if (null !== $locationType = $query->getData('location_type')) {
120
            $url .= '&location_type='.urlencode($locationType);
121
        }
122
123
        if (null !== $resultType = $query->getData('result_type')) {
124
            $url .= '&result_type='.urlencode($resultType);
125
        }
126
127
        return $this->fetchUrl($url, $query->getLocale(), $query->getLimit(), $query->getData('region', $this->region));
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133
    public function getName(): string
134
    {
135
        return 'google_maps';
136
    }
137
138
    /**
139
     * @param string $url
140
     * @param string $locale
0 ignored issues
show
Documentation introduced by
Should the type for parameter $locale not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
141
     *
142
     * @return string query with extra params
143
     */
144
    private function buildQuery(string $url, string $locale = null, string $region = null)
145
    {
146
        if (null !== $locale) {
147
            $url = sprintf('%s&language=%s', $url, $locale);
148
        }
149
150
        if (null !== $region) {
151
            $url = sprintf('%s&region=%s', $url, $region);
152
        }
153
154
        if (null !== $this->apiKey) {
155
            $url = sprintf('%s&key=%s', $url, $this->apiKey);
156
        }
157
158
        if (null !== $this->clientId) {
159
            $url = sprintf('%s&client=%s', $url, $this->clientId);
160
161
            if (null !== $this->privateKey) {
162
                $url = $this->signQuery($url);
163
            }
164
        }
165
166
        return $url;
167
    }
168
169
    /**
170
     * @param string $url
171
     * @param string $locale
0 ignored issues
show
Documentation introduced by
Should the type for parameter $locale not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
172
     * @param int    $limit
173
     * @param string $region
0 ignored issues
show
Documentation introduced by
Should the type for parameter $region not be null|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
174
     *
175
     * @return AddressCollection
176
     *
177
     * @throws InvalidServerResponse
178
     * @throws InvalidCredentials
179
     */
180
    private function fetchUrl(string $url, string $locale = null, int $limit, string $region = null): AddressCollection
181
    {
182
        $url = $this->buildQuery($url, $locale, $region);
183
        $content = $this->getUrlContents($url);
184
        $json = $this->validateResponse($url, $content);
185
186
        // no result
187
        if (!isset($json->results) || !count($json->results) || 'OK' !== $json->status) {
188
            return new AddressCollection([]);
189
        }
190
191
        $results = [];
192
        foreach ($json->results as $result) {
193
            $builder = new AddressBuilder($this->getName());
194
            $this->parseCoordinates($builder, $result);
195
196
            // set official Google place id
197
            if (isset($result->place_id)) {
198
                $builder->setValue('id', $result->place_id);
199
            }
200
201
            // update address components
202
            foreach ($result->address_components as $component) {
203
                foreach ($component->types as $type) {
204
                    $this->updateAddressComponent($builder, $type, $component);
205
                }
206
            }
207
208
            /** @var GoogleAddress $address */
209
            $address = $builder->build(GoogleAddress::class);
210
            $address = $address->withId($builder->getValue('id'));
211
            if (isset($result->geometry->location_type)) {
212
                $address = $address->withLocationType($result->geometry->location_type);
213
            }
214
            if (isset($result->types)) {
215
                $address = $address->withResultType($result->types);
216
            }
217
            if (isset($result->formatted_address)) {
218
                $address = $address->withFormattedAddress($result->formatted_address);
219
            }
220
            $address = $address->withStreetAddress($builder->getValue('street_address'));
221
            $address = $address->withIntersection($builder->getValue('intersection'));
222
            $address = $address->withPolitical($builder->getValue('political'));
223
            $address = $address->withColloquialArea($builder->getValue('colloquial_area'));
224
            $address = $address->withWard($builder->getValue('ward'));
225
            $address = $address->withNeighborhood($builder->getValue('neighborhood'));
226
            $address = $address->withPremise($builder->getValue('premise'));
227
            $address = $address->withSubpremise($builder->getValue('subpremise'));
228
            $address = $address->withNaturalFeature($builder->getValue('natural_feature'));
229
            $address = $address->withAirport($builder->getValue('airport'));
230
            $address = $address->withPark($builder->getValue('park'));
231
            $address = $address->withPointOfInterest($builder->getValue('point_of_interest'));
232
            $address = $address->withEstablishment($builder->getValue('establishment'));
233
            $address = $address->withSubLocalityLevels($builder->getValue('subLocalityLevel', []));
234
            $results[] = $address;
235
236
            if (count($results) >= $limit) {
237
                break;
238
            }
239
        }
240
241
        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
    private function updateAddressComponent(AddressBuilder $builder, string $type, $values)
252
    {
253
        switch ($type) {
254
            case 'postal_code':
255
                $builder->setPostalCode($values->long_name);
256
257
                break;
258
259
            case 'locality':
260
            case 'postal_town':
261
                $builder->setLocality($values->long_name);
262
263
                break;
264
265
            case 'administrative_area_level_1':
266
            case 'administrative_area_level_2':
267
            case 'administrative_area_level_3':
268
            case 'administrative_area_level_4':
269
            case 'administrative_area_level_5':
270
                $builder->addAdminLevel(intval(substr($type, -1)), $values->long_name, $values->short_name);
271
272
                break;
273
274
            case 'sublocality_level_1':
275
            case 'sublocality_level_2':
276
            case 'sublocality_level_3':
277
            case 'sublocality_level_4':
278
            case 'sublocality_level_5':
279
                $subLocalityLevel = $builder->getValue('subLocalityLevel', []);
280
                $subLocalityLevel[] = [
281
                    'level' => intval(substr($type, -1)),
282
                    'name' => $values->long_name,
283
                    'code' => $values->short_name,
284
                ];
285
                $builder->setValue('subLocalityLevel', $subLocalityLevel);
286
287
                break;
288
289
            case 'country':
290
                $builder->setCountry($values->long_name);
291
                $builder->setCountryCode($values->short_name);
292
293
                break;
294
295
            case 'street_number':
296
                $builder->setStreetNumber($values->long_name);
297
298
                break;
299
300
            case 'route':
301
                $builder->setStreetName($values->long_name);
302
303
                break;
304
305
            case 'sublocality':
306
                $builder->setSubLocality($values->long_name);
307
308
                break;
309
310
            case 'street_address':
311
            case 'intersection':
312
            case 'political':
313
            case 'colloquial_area':
314
            case 'ward':
315
            case 'neighborhood':
316
            case 'premise':
317
            case 'subpremise':
318
            case 'natural_feature':
319
            case 'airport':
320
            case 'park':
321
            case 'point_of_interest':
322
            case 'establishment':
323
                $builder->setValue($type, $values->long_name);
324
325
                break;
326
327
            default:
328
        }
329
    }
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
    private function signQuery(string $query): string
341
    {
342
        $url = parse_url($query);
343
344
        $urlPartToSign = $url['path'].'?'.$url['query'];
345
346
        // Decode the private key into its binary format
347
        $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
        $signature = hash_hmac('sha1', $urlPartToSign, $decodedKey, true);
352
353
        $encodedSignature = str_replace(['+', '/'], ['-', '_'], base64_encode($signature));
354
355
        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
    private function validateResponse(string $url, $content)
371
    {
372
        // Throw exception if invalid clientID and/or privateKey used with GoogleMapsBusinessProvider
373
        if (strpos($content, "Provided 'signature' is not valid for the provided client ID") !== false) {
374
            throw new InvalidCredentials(sprintf('Invalid client ID / API Key %s', $url));
375
        }
376
377
        $json = json_decode($content);
378
379
        // API error
380
        if (!isset($json)) {
381
            throw InvalidServerResponse::create($url);
382
        }
383
384
        if ('REQUEST_DENIED' === $json->status && 'The provided API key is invalid.' === $json->error_message) {
385
            throw new InvalidCredentials(sprintf('API key is invalid %s', $url));
386
        }
387
388
        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
        if ('OVER_QUERY_LIMIT' === $json->status) {
396
            throw new QuotaExceeded(sprintf('Daily quota exceeded %s', $url));
397
        }
398
399
        return $json;
400
    }
401
402
    /**
403
     * Parse coordinats and bounds.
404
     *
405
     * @param AddressBuilder $builder
406
     * @param $result
407
     */
408
    private function parseCoordinates(AddressBuilder $builder, $result)
409
    {
410
        $coordinates = $result->geometry->location;
411
        $builder->setCoordinates($coordinates->lat, $coordinates->lng);
412
413
        if (isset($result->geometry->bounds)) {
414
            $builder->setBounds(
415
                $result->geometry->bounds->southwest->lat,
416
                $result->geometry->bounds->southwest->lng,
417
                $result->geometry->bounds->northeast->lat,
418
                $result->geometry->bounds->northeast->lng
419
            );
420
        } elseif (isset($result->geometry->viewport)) {
421
            $builder->setBounds(
422
                $result->geometry->viewport->southwest->lat,
423
                $result->geometry->viewport->southwest->lng,
424
                $result->geometry->viewport->northeast->lat,
425
                $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
    }
437
}
438