Completed
Push — master ( 0a23f4...067236 )
by Tobias
23:34 queued 22:22
created

GoogleMaps   F

Complexity

Total Complexity 74

Size/Duplication

Total Lines 437
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 93.68%

Importance

Changes 0
Metric Value
wmc 74
lcom 1
cbo 12
dl 0
loc 437
ccs 178
cts 190
cp 0.9368
rs 2.48
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A business() 0 9 1
A __construct() 0 7 1
A geocodeQuery() 0 20 5
A reverseQuery() 0 15 3
A getName() 0 4 1
B buildQuery() 0 28 7
C fetchUrl() 0 64 12
D updateAddressComponent() 0 79 31
A signQuery() 0 17 1
A serializeComponents() 0 6 1
B validateResponse() 0 31 7
A parseCoordinates() 0 29 4

How to fix   Complexity   

Complex Class

Complex classes like GoogleMaps often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GoogleMaps, and based on these observations, apply Extract Interface, too.

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