Completed
Push — master ( 8e77a6...e96e3a )
by Tobias
03:14
created

Mapbox   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 390
Duplicated Lines 2.56 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 88%

Importance

Changes 0
Metric Value
wmc 41
lcom 1
cbo 10
dl 10
loc 390
ccs 110
cts 125
cp 0.88
rs 9.1199
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 2
B geocodeQuery() 5 33 6
A reverseQuery() 5 22 4
A getName() 0 4 1
A buildQuery() 0 13 2
C fetchUrl() 0 58 12
B updateAddressComponent() 0 45 9
A validateResponse() 0 11 3
A parseCoordinates() 0 14 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Mapbox 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 Mapbox, 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\Mapbox;
14
15
use Geocoder\Collection;
16
use Geocoder\Exception\InvalidArgument;
17
use Geocoder\Exception\InvalidServerResponse;
18
use Geocoder\Exception\UnsupportedOperation;
19
use Geocoder\Model\AddressCollection;
20
use Geocoder\Model\AddressBuilder;
21
use Geocoder\Query\GeocodeQuery;
22
use Geocoder\Query\ReverseQuery;
23
use Geocoder\Http\Provider\AbstractHttpProvider;
24
use Geocoder\Provider\Mapbox\Model\MapboxAddress;
25
use Geocoder\Provider\Provider;
26
use Http\Client\HttpClient;
27
28
final class Mapbox extends AbstractHttpProvider implements Provider
29
{
30
    /**
31
     * @var string
32
     */
33
    const GEOCODE_ENDPOINT_URL_SSL = 'https://api.mapbox.com/geocoding/v5/%s/%s.json';
34
35
    /**
36
     * @var string
37
     */
38
    const REVERSE_ENDPOINT_URL_SSL = 'https://api.mapbox.com/geocoding/v5/%s/%F,%F.json';
39
40
    /**
41
     * @var string
42
     */
43
    const GEOCODING_MODE_PLACES = 'mapbox.places';
44
45
    /**
46
     * @var string
47
     */
48
    const GEOCODING_MODE_PLACES_PERMANENT = 'mapbox.places-permanent';
49
50
    /**
51
     * @var array
52
     */
53
    const GEOCODING_MODES = [
54
        self::GEOCODING_MODE_PLACES,
55
        self::GEOCODING_MODE_PLACES_PERMANENT,
56
    ];
57
58
    /**
59
     * @var string
60
     */
61
    const TYPE_COUNTRY = 'country';
62
63
    /**
64
     * @var string
65
     */
66
    const TYPE_REGION = 'region';
67
68
    /**
69
     * @var string
70
     */
71
    const TYPE_POSTCODE = 'postcode';
72
73
    /**
74
     * @var string
75
     */
76
    const TYPE_DISTRICT = 'district';
77
78
    /**
79
     * @var string
80
     */
81
    const TYPE_PLACE = 'place';
82
83
    /**
84
     * @var string
85
     */
86
    const TYPE_LOCALITY = 'locality';
87
88
    /**
89
     * @var string
90
     */
91
    const TYPE_NEIGHBORHOOD = 'neighborhood';
92
93
    /**
94
     * @var string
95
     */
96
    const TYPE_ADDRESS = 'address';
97
98
    /**
99
     * @var string
100
     */
101
    const TYPE_POI = 'poi';
102
103
    /**
104
     * @var string
105
     */
106
    const TYPE_POI_LANDMARK = 'poi.landmark';
107
108
    /**
109
     * @var array
110
     */
111
    const TYPES = [
112
        self::TYPE_COUNTRY,
113
        self::TYPE_REGION,
114
        self::TYPE_POSTCODE,
115
        self::TYPE_DISTRICT,
116
        self::TYPE_PLACE,
117
        self::TYPE_LOCALITY,
118
        self::TYPE_NEIGHBORHOOD,
119
        self::TYPE_ADDRESS,
120
        self::TYPE_POI,
121
        self::TYPE_POI_LANDMARK,
122
    ];
123
124
    const DEFAULT_TYPE = self::TYPE_ADDRESS;
125
126
    /**
127
     * @var HttpClient
128
     */
129
    private $client;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
130
131
    /**
132
     * @var string
133
     */
134
    private $accessToken;
135
136
    /**
137
     * @var string|null
138
     */
139
    private $country;
140
141
    /**
142
     * @var string
143
     */
144
    private $geocodingMode;
145
146
    /**
147
     * @param HttpClient  $client        An HTTP adapter
148
     * @param string      $accessToken   Your Mapbox access token
149
     * @param string|null $country
150
     * @param string      $geocodingMode
151
     */
152 24
    public function __construct(
153
        HttpClient $client,
154
        string $accessToken,
155
        string $country = null,
156
        string $geocodingMode = self::GEOCODING_MODE_PLACES
157
    ) {
158 24
        parent::__construct($client);
159
160 24
        if (!in_array($geocodingMode, self::GEOCODING_MODES)) {
161
            throw new InvalidArgument('The Mapbox geocoding mode should be either mapbox.places or mapbox.places-permanent.');
162
        }
163
164 24
        $this->client = $client;
165 24
        $this->accessToken = $accessToken;
166 24
        $this->country = $country;
167 24
        $this->geocodingMode = $geocodingMode;
168 24
    }
169
170 14
    public function geocodeQuery(GeocodeQuery $query): Collection
171
    {
172
        // Mapbox API returns invalid data if IP address given
173
        // This API doesn't handle IPs
174 14
        if (filter_var($query->getText(), FILTER_VALIDATE_IP)) {
175 3
            throw new UnsupportedOperation('The Mapbox provider does not support IP addresses, only street addresses.');
176
        }
177
178 11
        $url = sprintf(self::GEOCODE_ENDPOINT_URL_SSL, $this->geocodingMode, rawurlencode($query->getText()));
179
180 11
        $urlParameters = [];
181 11
        if ($query->getBounds()) {
182
            $urlParameters['bbox'] = sprintf(
183
                '%s,%s|%s,%s',
184
                $query->getBounds()->getWest(),
185
                $query->getBounds()->getSouth(),
186
                $query->getBounds()->getEast(),
187
                $query->getBounds()->getNorth()
188
            );
189
        }
190
191 11 View Code Duplication
        if (null !== $locationType = $query->getData('location_type')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
192
            $urlParameters['types'] = is_array($locationType) ? implode(',', $locationType) : $locationType;
193
        } else {
194 11
            $urlParameters['types'] = self::DEFAULT_TYPE;
195
        }
196
197 11
        if ($urlParameters) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $urlParameters of type array<string,object|inte...le|string|null|boolean> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
198 11
            $url .= '?'.http_build_query($urlParameters);
199
        }
200
201 11
        return $this->fetchUrl($url, $query->getLimit(), $query->getLocale(), $query->getData('country', $this->country));
202
    }
203
204 9
    public function reverseQuery(ReverseQuery $query): Collection
205
    {
206 9
        $coordinate = $query->getCoordinates();
207 9
        $url = sprintf(
208 9
            self::REVERSE_ENDPOINT_URL_SSL,
209 9
            $this->geocodingMode,
210 9
            $coordinate->getLongitude(),
211 9
            $coordinate->getLatitude()
212
        );
213
214 9 View Code Duplication
        if (null !== $locationType = $query->getData('location_type')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
215
            $urlParameters['types'] = is_array($locationType) ? implode(',', $locationType) : $locationType;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$urlParameters was never initialized. Although not strictly required by PHP, it is generally a good practice to add $urlParameters = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
216
        } else {
217 9
            $urlParameters['types'] = self::DEFAULT_TYPE;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$urlParameters was never initialized. Although not strictly required by PHP, it is generally a good practice to add $urlParameters = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
218
        }
219
220 9
        if ($urlParameters) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $urlParameters of type array<string,object|inte...le|string|null|boolean> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
221 9
            $url .= '?'.http_build_query($urlParameters);
222
        }
223
224 9
        return $this->fetchUrl($url, $query->getLimit(), $query->getLocale(), $query->getData('country', $this->country));
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230 6
    public function getName(): string
231
    {
232 6
        return 'mapbox';
233
    }
234
235
    /**
236
     * @param string      $url
237
     * @param int         $limit
238
     * @param string|null $locale
239
     * @param string|null $country
240
     *
241
     * @return string query with extra params
242
     */
243 20
    private function buildQuery(string $url, int $limit, string $locale = null, string $country = null): string
244
    {
245 20
        $parameters = array_filter([
246 20
            'country' => $country,
247 20
            'language' => $locale,
248 20
            'limit' => $limit,
249 20
            'access_token' => $this->accessToken,
250
        ]);
251
252 20
        $separator = parse_url($url, PHP_URL_QUERY) ? '&' : '?';
253
254 20
        return $url.$separator.http_build_query($parameters);
255
    }
256
257
    /**
258
     * @param string      $url
259
     * @param int         $limit
260
     * @param string|null $locale
261
     * @param string|null $country
262
     *
263
     * @return AddressCollection
264
     */
265 20
    private function fetchUrl(string $url, int $limit, string $locale = null, string $country = null): AddressCollection
266
    {
267 20
        $url = $this->buildQuery($url, $limit, $locale, $country);
268 20
        $content = $this->getUrlContents($url);
269 7
        $json = $this->validateResponse($url, $content);
270
271
        // no result
272 7
        if (!isset($json['features']) || !count($json['features'])) {
273 1
            return new AddressCollection([]);
274
        }
275
276 6
        $results = [];
277 6
        foreach ($json['features'] as $result) {
278 6
            if (!array_key_exists('context', $result)) {
279 1
                break;
280
            }
281
282 5
            $builder = new AddressBuilder($this->getName());
283 5
            $this->parseCoordinates($builder, $result);
284
285
            // set official Mapbox place id
286 5
            if (isset($result['id'])) {
287 5
                $builder->setValue('id', $result['id']);
288
            }
289
290
            // set official Mapbox place id
291 5
            if (isset($result['text'])) {
292 5
                $builder->setValue('street_name', $result['text']);
293
            }
294
295
            // update address components
296 5
            foreach ($result['context'] as $component) {
297 5
                $this->updateAddressComponent($builder, $component['id'], $component);
298
            }
299
300
            /** @var MapboxAddress $address */
301 5
            $address = $builder->build(MapboxAddress::class);
302 5
            $address = $address->withId($builder->getValue('id'));
303 5
            if (isset($result['address'])) {
304 4
                $address = $address->withStreetNumber($result['address']);
305
            }
306 5
            if (isset($result['place_type'])) {
307 5
                $address = $address->withResultType($result['place_type']);
308
            }
309 5
            if (isset($result['place_name'])) {
310 5
                $address = $address->withFormattedAddress($result['place_name']);
311
            }
312 5
            $address = $address->withStreetName($builder->getValue('street_name'));
313 5
            $address = $address->withNeighborhood($builder->getValue('neighborhood'));
314 5
            $results[] = $address;
315
316 5
            if (count($results) >= $limit) {
317 5
                break;
318
            }
319
        }
320
321 6
        return new AddressCollection($results);
322
    }
323
324
    /**
325
     * Update current resultSet with given key/value.
326
     *
327
     * @param AddressBuilder $builder
328
     * @param string         $type    Component type
329
     * @param array          $value   The component value
330
     */
331 5
    private function updateAddressComponent(AddressBuilder $builder, string $type, array $value)
332
    {
333 5
        $typeParts = explode('.', $type);
334 5
        $type = reset($typeParts);
335
336 5
        switch ($type) {
337 5
            case 'postcode':
338 5
                $builder->setPostalCode($value['text']);
339
340 5
                break;
341
342 5
            case 'locality':
343 2
                $builder->setLocality($value['text']);
344
345 2
                break;
346
347 5
            case 'country':
348 5
                $builder->setCountry($value['text']);
349 5
                $builder->setCountryCode(strtoupper($value['short_code']));
350
351 5
                break;
352
353 5
            case 'neighborhood':
354 4
                $builder->setValue($type, $value['text']);
355
356 4
                break;
357
358 5
            case 'place':
359 5
                $builder->addAdminLevel(1, $value['text']);
360 5
                $builder->setLocality($value['text']);
361
362 5
                break;
363
364 4
            case 'region':
365 4
                $code = null;
366 4
                if (!empty($value['short_code']) && preg_match('/[A-z]{2}-/', $value['short_code'])) {
367 4
                    $code = preg_replace('/[A-z]{2}-/', '', $value['short_code']);
368
                }
369 4
                $builder->addAdminLevel(2, $value['text'], $code);
370
371 4
                break;
372
373
            default:
374
        }
375 5
    }
376
377
    /**
378
     * Decode the response content and validate it to make sure it does not have any errors.
379
     *
380
     * @param string $url
381
     * @param string $content
382
     *
383
     * @return array
384
     */
385 7
    private function validateResponse(string $url, $content): array
386
    {
387 7
        $json = json_decode($content, true);
388
389
        // API error
390 7
        if (!isset($json) || JSON_ERROR_NONE !== json_last_error()) {
391
            throw InvalidServerResponse::create($url);
392
        }
393
394 7
        return $json;
395
    }
396
397
    /**
398
     * Parse coordinats and bounds.
399
     *
400
     * @param AddressBuilder $builder
401
     * @param array          $result
402
     */
403 5
    private function parseCoordinates(AddressBuilder $builder, array $result)
404
    {
405 5
        $coordinates = $result['geometry']['coordinates'];
406 5
        $builder->setCoordinates($coordinates[1], $coordinates[0]);
407
408 5
        if (isset($result['bbox'])) {
409
            $builder->setBounds(
410
                $result['bbox'][1],
411
                $result['bbox'][0],
412
                $result['bbox'][3],
413
                $result['bbox'][2]
414
            );
415
        }
416 5
    }
417
}
418