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