Completed
Push — master ( 89f03c...796fa7 )
by Tobias
03:38
created

Mapbox.php (7 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 25
    public function __construct(
153
        HttpClient $client,
154
        string $accessToken,
155
        string $country = null,
156
        string $geocodingMode = self::GEOCODING_MODE_PLACES
157
    ) {
158 25
        parent::__construct($client);
159
160 25
        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 25
        $this->client = $client;
165 25
        $this->accessToken = $accessToken;
166 25
        $this->country = $country;
167 25
        $this->geocodingMode = $geocodingMode;
168 25
    }
169
170 15
    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 15
        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 12
        $url = sprintf(self::GEOCODE_ENDPOINT_URL_SSL, $this->geocodingMode, rawurlencode($query->getText()));
179
180 12
        $urlParameters = [];
181 12
        if ($query->getBounds()) {
182
            // Format is "minLon,minLat,maxLon,maxLat"
183 1
            $urlParameters['bbox'] = sprintf(
184 1
                '%s,%s,%s,%s',
185 1
                $query->getBounds()->getWest(),
186 1
                $query->getBounds()->getSouth(),
187 1
                $query->getBounds()->getEast(),
188 1
                $query->getBounds()->getNorth()
189
            );
190
        }
191
192 12 View Code Duplication
        if (null !== $locationType = $query->getData('location_type')) {
0 ignored issues
show
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...
193 1
            $urlParameters['types'] = is_array($locationType) ? implode(',', $locationType) : $locationType;
194
        } else {
195 11
            $urlParameters['types'] = self::DEFAULT_TYPE;
196
        }
197
198 12
        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...
199 12
            $url .= '?'.http_build_query($urlParameters);
200
        }
201
202 12
        return $this->fetchUrl($url, $query->getLimit(), $query->getLocale(), $query->getData('country', $this->country));
203
    }
204
205 9
    public function reverseQuery(ReverseQuery $query): Collection
206
    {
207 9
        $coordinate = $query->getCoordinates();
208 9
        $url = sprintf(
209 9
            self::REVERSE_ENDPOINT_URL_SSL,
210 9
            $this->geocodingMode,
211 9
            $coordinate->getLongitude(),
212 9
            $coordinate->getLatitude()
213
        );
214
215 9 View Code Duplication
        if (null !== $locationType = $query->getData('location_type')) {
0 ignored issues
show
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...
216
            $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...
217
        } else {
218 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...
219
        }
220
221 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...
222 9
            $url .= '?'.http_build_query($urlParameters);
223
        }
224
225 9
        return $this->fetchUrl($url, $query->getLimit(), $query->getLocale(), $query->getData('country', $this->country));
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231 7
    public function getName(): string
232
    {
233 7
        return 'mapbox';
234
    }
235
236
    /**
237
     * @param string      $url
238
     * @param int         $limit
239
     * @param string|null $locale
240
     * @param string|null $country
241
     *
242
     * @return string query with extra params
243
     */
244 21
    private function buildQuery(string $url, int $limit, string $locale = null, string $country = null): string
245
    {
246 21
        $parameters = array_filter([
247 21
            'country' => $country,
248 21
            'language' => $locale,
249 21
            'limit' => $limit,
250 21
            'access_token' => $this->accessToken,
251
        ]);
252
253 21
        $separator = parse_url($url, PHP_URL_QUERY) ? '&' : '?';
254
255 21
        return $url.$separator.http_build_query($parameters);
256
    }
257
258
    /**
259
     * @param string      $url
260
     * @param int         $limit
261
     * @param string|null $locale
262
     * @param string|null $country
263
     *
264
     * @return AddressCollection
265
     */
266 21
    private function fetchUrl(string $url, int $limit, string $locale = null, string $country = null): AddressCollection
267
    {
268 21
        $url = $this->buildQuery($url, $limit, $locale, $country);
269 21
        $content = $this->getUrlContents($url);
270 8
        $json = $this->validateResponse($url, $content);
271
272
        // no result
273 8
        if (!isset($json['features']) || !count($json['features'])) {
274 1
            return new AddressCollection([]);
275
        }
276
277 7
        $results = [];
278 7
        foreach ($json['features'] as $result) {
279 7
            if (!array_key_exists('context', $result)) {
280 1
                break;
281
            }
282
283 6
            $builder = new AddressBuilder($this->getName());
284 6
            $this->parseCoordinates($builder, $result);
285
286
            // set official Mapbox place id
287 6
            if (isset($result['id'])) {
288 6
                $builder->setValue('id', $result['id']);
289
            }
290
291
            // set official Mapbox place id
292 6
            if (isset($result['text'])) {
293 6
                $builder->setValue('street_name', $result['text']);
294
            }
295
296
            // update address components
297 6
            foreach ($result['context'] as $component) {
298 6
                $this->updateAddressComponent($builder, $component['id'], $component);
299
            }
300
301
            /** @var MapboxAddress $address */
302 6
            $address = $builder->build(MapboxAddress::class);
303 6
            $address = $address->withId($builder->getValue('id'));
304 6
            if (isset($result['address'])) {
305 4
                $address = $address->withStreetNumber($result['address']);
306
            }
307 6
            if (isset($result['place_type'])) {
308 6
                $address = $address->withResultType($result['place_type']);
309
            }
310 6
            if (isset($result['place_name'])) {
311 6
                $address = $address->withFormattedAddress($result['place_name']);
312
            }
313 6
            $address = $address->withStreetName($builder->getValue('street_name'));
314 6
            $address = $address->withNeighborhood($builder->getValue('neighborhood'));
315 6
            $results[] = $address;
316
317 6
            if (count($results) >= $limit) {
318 4
                break;
319
            }
320
        }
321
322 7
        return new AddressCollection($results);
323
    }
324
325
    /**
326
     * Update current resultSet with given key/value.
327
     *
328
     * @param AddressBuilder $builder
329
     * @param string         $type    Component type
330
     * @param array          $value   The component value
331
     */
332 6
    private function updateAddressComponent(AddressBuilder $builder, string $type, array $value)
333
    {
334 6
        $typeParts = explode('.', $type);
335 6
        $type = reset($typeParts);
336
337 6
        switch ($type) {
338 6
            case 'postcode':
339 5
                $builder->setPostalCode($value['text']);
340
341 5
                break;
342
343 6
            case 'locality':
344 2
                $builder->setLocality($value['text']);
345
346 2
                break;
347
348 6
            case 'country':
349 6
                $builder->setCountry($value['text']);
350 6
                if (isset($value['short_code'])) {
351 5
                    $builder->setCountryCode(strtoupper($value['short_code']));
352
                }
353
354 6
                break;
355
356 5
            case 'neighborhood':
357 4
                $builder->setValue($type, $value['text']);
358
359 4
                break;
360
361 5
            case 'place':
362 5
                $builder->addAdminLevel(1, $value['text']);
363 5
                $builder->setLocality($value['text']);
364
365 5
                break;
366
367 4
            case 'region':
368 4
                $code = null;
369 4
                if (!empty($value['short_code']) && preg_match('/[A-z]{2}-/', $value['short_code'])) {
370 4
                    $code = preg_replace('/[A-z]{2}-/', '', $value['short_code']);
371
                }
372 4
                $builder->addAdminLevel(2, $value['text'], $code);
373
374 4
                break;
375
376
            default:
377
        }
378 6
    }
379
380
    /**
381
     * Decode the response content and validate it to make sure it does not have any errors.
382
     *
383
     * @param string $url
384
     * @param string $content
385
     *
386
     * @return array
387
     */
388 8
    private function validateResponse(string $url, $content): array
389
    {
390 8
        $json = json_decode($content, true);
391
392
        // API error
393 8
        if (!isset($json) || JSON_ERROR_NONE !== json_last_error()) {
394
            throw InvalidServerResponse::create($url);
395
        }
396
397 8
        return $json;
398
    }
399
400
    /**
401
     * Parse coordinats and bounds.
402
     *
403
     * @param AddressBuilder $builder
404
     * @param array          $result
405
     */
406 6
    private function parseCoordinates(AddressBuilder $builder, array $result)
407
    {
408 6
        $coordinates = $result['geometry']['coordinates'];
409 6
        $builder->setCoordinates($coordinates[1], $coordinates[0]);
410
411 6
        if (isset($result['bbox'])) {
412 1
            $builder->setBounds(
413 1
                $result['bbox'][1],
414 1
                $result['bbox'][0],
415 1
                $result['bbox'][3],
416 1
                $result['bbox'][2]
417
            );
418
        }
419 6
    }
420
}
421