Completed
Push — master ( 7a9f2a...3c69d9 )
by Tobias
01:59
created

GoogleMaps   C

Complexity

Total Complexity 62

Size/Duplication

Total Lines 349
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 92.36%

Importance

Changes 0
Metric Value
wmc 62
lcom 1
cbo 12
dl 0
loc 349
ccs 145
cts 157
cp 0.9236
rs 5.9493
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A business() 0 8 1
A __construct() 0 7 1
A geocodeQuery() 0 15 3
A reverseQuery() 0 15 3
A getName() 0 4 1
B buildQuery() 0 24 6
D fetchUrl() 0 109 20
C updateAddressComponent() 0 56 26
A signQuery() 0 17 1

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
     * 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 39
    public function __construct(HttpClient $client, string $region = null, string $apiKey = null)
91
    {
92 39
        parent::__construct($client);
93
94 39
        $this->region = $region;
95 39
        $this->apiKey = $apiKey;
96 39
    }
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 10
    public function reverseQuery(ReverseQuery $query): Collection
115
    {
116 10
        $coordinate = $query->getCoordinates();
117 10
        $url = sprintf(self::REVERSE_ENDPOINT_URL_SSL, $coordinate->getLatitude(), $coordinate->getLongitude());
118
119 10
        if (null !== $locationType = $query->getData('location_type')) {
120
            $url .= '&location_type='.urlencode($locationType);
121
        }
122
123 10
        if (null !== $resultType = $query->getData('result_type')) {
124
            $url .= '&result_type='.urlencode($resultType);
125
        }
126
127 10
        return $this->fetchUrl($url, $query->getLocale(), $query->getLimit(), $query->getData('region', $this->region));
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133 16
    public function getName(): string
134
    {
135 16
        return 'google_maps';
136
    }
137
138
    /**
139
     * @param string $url
140
     * @param string $locale
141
     *
142
     * @return string query with extra params
143
     */
144 35
    private function buildQuery(string $url, string $locale = null, string $region = null)
145
    {
146 35
        if (null !== $locale) {
147 4
            $url = sprintf('%s&language=%s', $url, $locale);
148
        }
149
150 35
        if (null !== $region) {
151 1
            $url = sprintf('%s&region=%s', $url, $region);
152
        }
153
154 35
        if (null !== $this->apiKey) {
155 2
            $url = sprintf('%s&key=%s', $url, $this->apiKey);
156
        }
157
158 35
        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 35
        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 35
    private function fetchUrl(string $url, string $locale = null, int $limit, string $region = null): AddressCollection
181
    {
182 35
        $url = $this->buildQuery($url, $locale, $region);
183 35
        $content = $this->getUrlContents($url);
184
185
        // Throw exception if invalid clientID and/or privateKey used with GoogleMapsBusinessProvider
186 22
        if (strpos($content, "Provided 'signature' is not valid for the provided client ID") !== false) {
187 2
            throw new InvalidCredentials(sprintf('Invalid client ID / API Key %s', $url));
188
        }
189
190 20
        $json = json_decode($content);
191
192
        // API error
193 20
        if (!isset($json)) {
194
            throw InvalidServerResponse::create($url);
195
        }
196
197 20
        if ('REQUEST_DENIED' === $json->status && 'The provided API key is invalid.' === $json->error_message) {
198 2
            throw new InvalidCredentials(sprintf('API key is invalid %s', $url));
199
        }
200
201 18
        if ('REQUEST_DENIED' === $json->status) {
202
            throw new InvalidServerResponse(
203
                sprintf('API access denied. Request: %s - Message: %s', $url, $json->error_message)
204
            );
205
        }
206
207
        // you are over your quota
208 18
        if ('OVER_QUERY_LIMIT' === $json->status) {
209 1
            throw new QuotaExceeded(sprintf('Daily quota exceeded %s', $url));
210
        }
211
212
        // no result
213 17
        if (!isset($json->results) || !count($json->results) || 'OK' !== $json->status) {
214 2
            return new AddressCollection([]);
215
        }
216
217 15
        $results = [];
218 15
        foreach ($json->results as $result) {
219 15
            $builder = new AddressBuilder($this->getName());
220
221
            // update address components
222 15
            foreach ($result->address_components as $component) {
223 15
                foreach ($component->types as $type) {
224 15
                    $this->updateAddressComponent($builder, $type, $component);
225
                }
226
            }
227
228
            // update coordinates
229 15
            $coordinates = $result->geometry->location;
230 15
            $builder->setCoordinates($coordinates->lat, $coordinates->lng);
231
232 15
            if (isset($result->geometry->bounds)) {
233 8
                $builder->setBounds(
234 8
                    $result->geometry->bounds->southwest->lat,
235 8
                    $result->geometry->bounds->southwest->lng,
236 8
                    $result->geometry->bounds->northeast->lat,
237 8
                    $result->geometry->bounds->northeast->lng
238
                );
239 10
            } elseif (isset($result->geometry->viewport)) {
240 10
                $builder->setBounds(
241 10
                    $result->geometry->viewport->southwest->lat,
242 10
                    $result->geometry->viewport->southwest->lng,
243 10
                    $result->geometry->viewport->northeast->lat,
244 10
                    $result->geometry->viewport->northeast->lng
245
                );
246
            } elseif ('ROOFTOP' === $result->geometry->location_type) {
247
                // Fake bounds
248
                $builder->setBounds(
249
                    $coordinates->lat,
250
                    $coordinates->lng,
251
                    $coordinates->lat,
252
                    $coordinates->lng
253
                );
254
            }
255
256
            /** @var GoogleAddress $address */
257 15
            $address = $builder->build(GoogleAddress::class);
258 15
            if (isset($result->geometry->location_type)) {
259 15
                $address = $address->withLocationType($result->geometry->location_type);
260
            }
261 15
            if (isset($result->types)) {
262 15
                $address = $address->withResultType($result->types);
263
            }
264 15
            if (isset($result->formatted_address)) {
265 15
                $address = $address->withFormattedAddress($result->formatted_address);
266
            }
267 15
            $address = $address->withStreetAddress($builder->getValue('street_address'));
268 15
            $address = $address->withIntersection($builder->getValue('intersection'));
269 15
            $address = $address->withPolitical($builder->getValue('political'));
270 15
            $address = $address->withColloquialArea($builder->getValue('colloquial_area'));
271 15
            $address = $address->withWard($builder->getValue('ward'));
272 15
            $address = $address->withNeighborhood($builder->getValue('neighborhood'));
273 15
            $address = $address->withPremise($builder->getValue('premise'));
274 15
            $address = $address->withSubpremise($builder->getValue('subpremise'));
275 15
            $address = $address->withNaturalFeature($builder->getValue('natural_feature'));
276 15
            $address = $address->withAirport($builder->getValue('airport'));
277 15
            $address = $address->withPark($builder->getValue('park'));
278 15
            $address = $address->withPointOfInterest($builder->getValue('point_of_interest'));
279 15
            $address = $address->withEstablishment($builder->getValue('establishment'));
280 15
            $results[] = $address;
281
282 15
            if (count($results) >= $limit) {
283 15
                break;
284
            }
285
        }
286
287 15
        return new AddressCollection($results);
288
    }
289
290
    /**
291
     * Update current resultSet with given key/value.
292
     *
293
     * @param AddressBuilder $builder
294
     * @param string         $type    Component type
295
     * @param object         $values  The component values
296
     */
297 15
    private function updateAddressComponent(AddressBuilder $builder, string $type, $values)
298
    {
299
        switch ($type) {
300 15
            case 'postal_code':
301 13
                $builder->setPostalCode($values->long_name);
302 13
                break;
303
304 15
            case 'locality':
305 15
            case 'postal_town':
306 15
                $builder->setLocality($values->long_name);
307 15
                break;
308
309 15
            case 'administrative_area_level_1':
310 15
            case 'administrative_area_level_2':
311 15
            case 'administrative_area_level_3':
312 15
            case 'administrative_area_level_4':
313 15
            case 'administrative_area_level_5':
314 14
                $builder->addAdminLevel(intval(substr($type, -1)), $values->long_name, $values->short_name);
315 14
                break;
316
317 15
            case 'country':
318 15
                $builder->setCountry($values->long_name);
319 15
                $builder->setCountryCode($values->short_name);
320 15
                break;
321
322 15
            case 'street_number':
323 7
                $builder->setStreetNumber($values->long_name);
324 7
                break;
325
326 15
            case 'route':
327 8
                $builder->setStreetName($values->long_name);
328 8
                break;
329
330 15
            case 'sublocality':
331 5
                $builder->setSubLocality($values->long_name);
332 5
                break;
333
334 15
            case 'street_address':
335 15
            case 'intersection':
336 15
            case 'political':
337 13
            case 'colloquial_area':
338 12
            case 'ward':
339 12
            case 'neighborhood':
340 11
            case 'premise':
341 10
            case 'subpremise':
342 9
            case 'natural_feature':
343 9
            case 'airport':
344 9
            case 'park':
345 9
            case 'point_of_interest':
346 9
            case 'establishment':
347 15
                $builder->setValue($type, $values->long_name);
348 15
                break;
349
350
            default:
351
        }
352 15
    }
353
354
    /**
355
     * Sign a URL with a given crypto key
356
     * Note that this URL must be properly URL-encoded
357
     * src: http://gmaps-samples.googlecode.com/svn/trunk/urlsigning/UrlSigner.php-source.
358
     *
359
     * @param string $query Query to be signed
360
     *
361
     * @return string $query query with signature appended
362
     */
363 3
    private function signQuery(string $query): string
364
    {
365 3
        $url = parse_url($query);
366
367 3
        $urlPartToSign = $url['path'].'?'.$url['query'];
368
369
        // Decode the private key into its binary format
370 3
        $decodedKey = base64_decode(str_replace(['-', '_'], ['+', '/'], $this->privateKey));
371
372
        // Create a signature using the private key and the URL-encoded
373
        // string using HMAC SHA1. This signature will be binary.
374 3
        $signature = hash_hmac('sha1', $urlPartToSign, $decodedKey, true);
375
376 3
        $encodedSignature = str_replace(['+', '/'], ['-', '_'], base64_encode($signature));
377
378 3
        return sprintf('%s&signature=%s', $query, $encodedSignature);
379
    }
380
}
381