GoogleMapsPlaces   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 430
Duplicated Lines 0 %

Importance

Changes 2
Bugs 1 Features 1
Metric Value
wmc 60
eloc 161
c 2
b 1
f 1
dl 0
loc 430
rs 3.6

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A reverseQuery() 0 10 2
A buildPlaceSearchQuery() 0 24 4
A buildFindPlaceQuery() 0 28 4
B buildNearbySearchQuery() 0 64 11
A applyDataFromQuery() 0 11 3
B validateResponse() 0 26 7
A parseCoordinates() 0 11 2
A geocodeQuery() 0 15 4
F fetchUrl() 0 92 21
A getName() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like GoogleMapsPlaces 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.

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 GoogleMapsPlaces, 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\GoogleMapsPlaces;
14
15
use Geocoder\Collection;
16
use Geocoder\Exception\InvalidArgument;
17
use Geocoder\Exception\InvalidCredentials;
18
use Geocoder\Exception\InvalidServerResponse;
19
use Geocoder\Exception\QuotaExceeded;
20
use Geocoder\Exception\UnsupportedOperation;
21
use Geocoder\Http\Provider\AbstractHttpProvider;
22
use Geocoder\Model\AddressBuilder;
23
use Geocoder\Model\AddressCollection;
24
use Geocoder\Provider\GoogleMapsPlaces\Model\GooglePlace;
25
use Geocoder\Provider\GoogleMapsPlaces\Model\OpeningHours;
26
use Geocoder\Provider\GoogleMapsPlaces\Model\Photo;
27
use Geocoder\Provider\GoogleMapsPlaces\Model\PlusCode;
28
use Geocoder\Provider\Provider;
29
use Geocoder\Query\GeocodeQuery;
30
use Geocoder\Query\Query;
31
use Geocoder\Query\ReverseQuery;
32
use Http\Client\HttpClient;
33
use stdClass;
34
35
/**
36
 * @author atymic <[email protected]>
37
 */
38
final class GoogleMapsPlaces extends AbstractHttpProvider implements Provider
39
{
40
    /**
41
     * @var string
42
     */
43
    const SEARCH_ENDPOINT_URL_SSL = 'https://maps.googleapis.com/maps/api/place/textsearch/json';
44
45
    /**
46
     * @var string
47
     */
48
    const FIND_ENDPOINT_URL_SSL = 'https://maps.googleapis.com/maps/api/place/findplacefromtext/json';
49
50
    /**
51
     * @var string
52
     */
53
    const NEARBY_ENDPOINT_URL_SSL = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json';
54
55
    /**
56
     * @var string
57
     */
58
    const GEOCODE_MODE_FIND = 'find';
59
60
    /**
61
     * @var string
62
     */
63
    const GEOCODE_MODE_SEARCH = 'search';
64
65
    /**
66
     * @var string
67
     */
68
    const GEOCODE_MODE_NEARBY = 'nearby';
69
70
    /**
71
     * @var string
72
     */
73
    const DEFAULT_GEOCODE_MODE = self::GEOCODE_MODE_FIND;
74
75
    /**
76
     * @var string
77
     */
78
    const DEFAULT_FIELDS = 'formatted_address,geometry,icon,name,permanently_closed,photos,place_id,plus_code,types';
79
80
    /**
81
     * @var string|null
82
     */
83
    private $apiKey;
84
85
    /**
86
     * @param HttpClient $client An HTTP adapter
87
     * @param string     $apiKey Google Maps Places API Key
88
     */
89
    public function __construct(HttpClient $client, string $apiKey)
90
    {
91
        parent::__construct($client);
92
93
        $this->apiKey = $apiKey;
94
    }
95
96
    /**
97
     * @param GeocodeQuery $query
98
     *
99
     * @return Collection
100
     *
101
     * @throws UnsupportedOperation
102
     * @throws InvalidArgument
103
     */
104
    public function geocodeQuery(GeocodeQuery $query): Collection
105
    {
106
        if (filter_var($query->getText(), FILTER_VALIDATE_IP)) {
107
            throw new UnsupportedOperation('The GoogleMapsPlaces provider does not support IP addresses');
108
        }
109
110
        if (self::GEOCODE_MODE_FIND === $query->getData('mode', self::DEFAULT_GEOCODE_MODE)) {
111
            return $this->fetchUrl(self::FIND_ENDPOINT_URL_SSL, $this->buildFindPlaceQuery($query));
112
        }
113
114
        if (self::GEOCODE_MODE_SEARCH === $query->getData('mode', self::DEFAULT_GEOCODE_MODE)) {
115
            return $this->fetchUrl(self::SEARCH_ENDPOINT_URL_SSL, $this->buildPlaceSearchQuery($query));
116
        }
117
118
        throw new InvalidArgument(sprintf('Mode must be one of `%s, %s`', self::GEOCODE_MODE_FIND, self::GEOCODE_MODE_SEARCH));
119
    }
120
121
    /**
122
     * @param ReverseQuery $query
123
     *
124
     * @return Collection
125
     *
126
     * @throws InvalidArgument
127
     */
128
    public function reverseQuery(ReverseQuery $query): Collection
129
    {
130
        // for backward compatibility: use SEARCH as default mode (includes formatted_address)
131
        if (self::GEOCODE_MODE_SEARCH === $query->getData('mode', self::GEOCODE_MODE_SEARCH)) {
132
            $url = self::SEARCH_ENDPOINT_URL_SSL;
133
        } else {
134
            $url = self::NEARBY_ENDPOINT_URL_SSL;
135
        }
136
137
        return $this->fetchUrl($url, $this->buildNearbySearchQuery($query));
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143
    public function getName(): string
144
    {
145
        return 'google_maps_places';
146
    }
147
148
    /**
149
     * Build query for the find place API.
150
     *
151
     * @param GeocodeQuery $geocodeQuery
152
     *
153
     * @return array
154
     */
155
    private function buildFindPlaceQuery(GeocodeQuery $geocodeQuery): array
156
    {
157
        $query = [
158
            'input' => $geocodeQuery->getText(),
159
            'inputtype' => 'textquery',
160
            'fields' => self::DEFAULT_FIELDS,
161
        ];
162
163
        if (null !== $geocodeQuery->getLocale()) {
164
            $query['language'] = $geocodeQuery->getLocale();
165
        }
166
167
        // If query has bounds, set location bias to those bounds
168
        if (null !== $bounds = $geocodeQuery->getBounds()) {
169
            $query['locationbias'] = sprintf(
170
                'rectangle:%s,%s|%s,%s',
171
                $bounds->getSouth(),
172
                $bounds->getWest(),
173
                $bounds->getNorth(),
174
                $bounds->getEast()
175
            );
176
        }
177
178
        if (null !== $geocodeQuery->getData('fields')) {
179
            $query['fields'] = $geocodeQuery->getData('fields');
180
        }
181
182
        return $query;
183
    }
184
185
    /**
186
     * Build query for the place search API.
187
     *
188
     * @param GeocodeQuery $geocodeQuery
189
     *
190
     * @return array
191
     */
192
    private function buildPlaceSearchQuery(GeocodeQuery $geocodeQuery): array
193
    {
194
        $query = [
195
            'query' => $geocodeQuery->getText(),
196
        ];
197
198
        if (null !== $geocodeQuery->getLocale()) {
199
            $query['language'] = $geocodeQuery->getLocale();
200
        }
201
202
        $query = $this->applyDataFromQuery($geocodeQuery, $query, [
203
            'region',
204
            'type',
205
            'opennow',
206
            'minprice',
207
            'maxprice',
208
        ]);
209
210
        if (null !== $geocodeQuery->getData('location') && null !== $geocodeQuery->getData('radius')) {
211
            $query['location'] = (string) $geocodeQuery->getData('location');
212
            $query['radius'] = (int) $geocodeQuery->getData('radius');
213
        }
214
215
        return $query;
216
    }
217
218
    /**
219
     * Build query for the nearby search api.
220
     *
221
     * @param ReverseQuery $reverseQuery
222
     *
223
     * @return array
224
     */
225
    private function buildNearbySearchQuery(ReverseQuery $reverseQuery): array
226
    {
227
        // for backward compatibility: use SEARCH as default mode (includes formatted_address)
228
        $mode = $reverseQuery->getData('mode', self::GEOCODE_MODE_SEARCH);
229
230
        $query = [
231
            'location' => sprintf(
232
                '%s,%s',
233
                $reverseQuery->getCoordinates()->getLatitude(),
234
                $reverseQuery->getCoordinates()->getLongitude()
235
            ),
236
            'rankby' => 'prominence',
237
        ];
238
239
        if (null !== $reverseQuery->getLocale()) {
0 ignored issues
show
introduced by
The condition null !== $reverseQuery->getLocale() is always true.
Loading history...
240
            $query['language'] = $reverseQuery->getLocale();
241
        }
242
243
        $validParameters = [
244
            'keyword',
245
            'type',
246
            'name',
247
            'minprice',
248
            'maxprice',
249
            'name',
250
            'opennow',
251
            'radius',
252
        ];
253
254
        if (self::GEOCODE_MODE_NEARBY === $mode) {
255
            $validParameters[] = 'rankby';
256
        }
257
258
        $query = $this->applyDataFromQuery($reverseQuery, $query, $validParameters);
259
260
        if (self::GEOCODE_MODE_NEARBY === $mode) {
261
            // mode:nearby, rankby:prominence, parameter:radius
262
            if ('prominence' === $query['rankby'] && !isset($query['radius'])) {
263
                throw new InvalidArgument('`radius` is required to be set in the Query data for Reverse Geocoding when ranking by prominence');
264
            }
265
266
            // mode:nearby, rankby:distance, parameter:type/keyword/name
267
            if ('distance' === $query['rankby']) {
268
                if (isset($query['radius'])) {
269
                    unset($query['radius']);
270
                }
271
272
                $requiredParameters = array_intersect(['keyword', 'type', 'name'], array_keys($query));
273
274
                if (1 !== count($requiredParameters)) {
275
                    throw new InvalidArgument('One of `type`, `keyword`, `name` is required to be set in the Query data for Reverse Geocoding when ranking by distance');
276
                }
277
            }
278
        }
279
280
        if (self::GEOCODE_MODE_SEARCH === $mode) {
281
            // mode:search, parameter:type
282
283
            if (!isset($query['type'])) {
284
                throw new InvalidArgument('`type` is required to be set in the Query data for Reverse Geocoding when using search mode');
285
            }
286
        }
287
288
        return $query;
289
    }
290
291
    /**
292
     * @param Query $query
293
     * @param array $request
294
     * @param array $keys
295
     *
296
     * @return array
297
     */
298
    private function applyDataFromQuery(Query $query, array $request, array $keys)
299
    {
300
        foreach ($keys as $key) {
301
            if (null === $query->getData($key)) {
302
                continue;
303
            }
304
305
            $request[$key] = $query->getData($key);
306
        }
307
308
        return $request;
309
    }
310
311
    /**
312
     * @param string $url
313
     * @param array  $query
314
     *
315
     * @return AddressCollection
316
     */
317
    private function fetchUrl(string $url, array $query): AddressCollection
318
    {
319
        $query['key'] = $this->apiKey;
320
321
        $url = sprintf('%s?%s', $url, http_build_query($query));
322
323
        $content = $this->getUrlContents($url);
324
        $json = $this->validateResponse($url, $content);
325
326
        if (empty($json->candidates) && empty($json->results) || 'OK' !== $json->status) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (empty($json->candidates... 'OK' !== $json->status, Probably Intended Meaning: empty($json->candidates)...'OK' !== $json->status)
Loading history...
327
            return new AddressCollection([]);
328
        }
329
330
        $results = [];
331
332
        $apiResults = isset($json->results) ? $json->results : $json->candidates;
333
334
        foreach ($apiResults as $result) {
335
            $builder = new AddressBuilder($this->getName());
336
            $this->parseCoordinates($builder, $result);
337
338
            if (isset($result->place_id)) {
339
                $builder->setValue('id', $result->place_id);
340
            }
341
342
            /** @var GooglePlace $address */
343
            $address = $builder->build(GooglePlace::class);
344
            $address = $address->withId($builder->getValue('id'));
345
346
            if (isset($result->name)) {
347
                $address = $address->withName($result->name);
348
            }
349
350
            if (isset($result->formatted_address)) {
351
                $address = $address->withFormattedAddress($result->formatted_address);
352
            }
353
354
            if (isset($result->vicinity)) {
355
                $address = $address->withVicinity($result->vicinity);
356
            }
357
358
            if (isset($result->types)) {
359
                $address = $address->withType($result->types);
360
            }
361
362
            if (isset($result->icon)) {
363
                $address = $address->withIcon($result->icon);
364
            }
365
366
            if (isset($result->plus_code)) {
367
                $address = $address->withPlusCode(new PlusCode(
368
                    $result->plus_code->global_code,
369
                    $result->plus_code->compound_code
370
                ));
371
            }
372
373
            if (isset($result->photos)) {
374
                $address = $address->withPhotos(Photo::getPhotosFromResult($result->photos));
375
            }
376
377
            if (isset($result->price_level)) {
378
                $address = $address->withPriceLevel($result->price_level);
379
            }
380
381
            if (isset($result->rating)) {
382
                $address = $address->withRating((float) $result->rating);
383
            }
384
385
            if (isset($result->formatted_phone_number)) {
386
                $address = $address->withFormattedPhoneNumber($result->formatted_phone_number);
387
            }
388
389
            if (isset($result->international_phone_number)) {
390
                $address = $address->withInternationalPhoneNumber($result->international_phone_number);
391
            }
392
393
            if (isset($result->website)) {
394
                $address = $address->withWebsite($result->website);
395
            }
396
397
            if (isset($result->opening_hours)) {
398
                $address = $address->withOpeningHours(OpeningHours::fromResult($result->opening_hours));
399
            }
400
401
            if (isset($result->permanently_closed)) {
402
                $address = $address->setPermanentlyClosed();
403
            }
404
405
            $results[] = $address;
406
        }
407
408
        return new AddressCollection($results);
409
    }
410
411
    /**
412
     * Decode the response content and validate it to make sure it does not have any errors.
413
     *
414
     * @param string $url
415
     * @param string $content
416
     *
417
     * @return \StdClass
418
     *
419
     * @throws InvalidCredentials
420
     * @throws InvalidServerResponse
421
     * @throws QuotaExceeded
422
     */
423
    private function validateResponse(string $url, $content): StdClass
424
    {
425
        $json = json_decode($content);
426
427
        // API error
428
        if (!isset($json)) {
429
            throw InvalidServerResponse::create($url);
430
        }
431
432
        if ('INVALID_REQUEST' === $json->status) {
433
            throw new InvalidArgument(sprintf('Invalid Request %s', $url));
434
        }
435
436
        if ('REQUEST_DENIED' === $json->status && 'The provided API key is invalid.' === $json->error_messages) {
437
            throw new InvalidCredentials(sprintf('API key is invalid %s', $url));
438
        }
439
440
        if ('REQUEST_DENIED' === $json->status) {
441
            throw new InvalidServerResponse(sprintf('API access denied. Request: %s - Message: %s', $url, $json->error_messages));
442
        }
443
444
        if ('OVER_QUERY_LIMIT' === $json->status) {
445
            throw new QuotaExceeded(sprintf('Daily quota exceeded %s', $url));
446
        }
447
448
        return $json;
449
    }
450
451
    /**
452
     * Parse coordinates and bounds.
453
     *
454
     * @param AddressBuilder $builder
455
     * @param StdClass       $result
456
     */
457
    private function parseCoordinates(AddressBuilder $builder, StdClass $result)
458
    {
459
        $coordinates = $result->geometry->location;
460
        $builder->setCoordinates($coordinates->lat, $coordinates->lng);
461
462
        if (isset($result->geometry->viewport)) {
463
            $builder->setBounds(
464
                $result->geometry->viewport->southwest->lat,
465
                $result->geometry->viewport->southwest->lng,
466
                $result->geometry->viewport->northeast->lat,
467
                $result->geometry->viewport->northeast->lng
468
            );
469
        }
470
    }
471
}
472