Completed
Push — master ( 8dbb16...92d39f )
by Tobias
07:23
created

MapQuest::mapAddressToArray()   C

Complexity

Conditions 13
Paths 240

Size

Total Lines 59

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 13.7211

Importance

Changes 0
Metric Value
dl 0
loc 59
ccs 31
cts 37
cp 0.8378
rs 5.2833
c 0
b 0
f 0
cc 13
nc 240
nop 1
crap 13.7211

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\MapQuest;
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\Location;
23
use Geocoder\Model\Address;
24
use Geocoder\Model\AddressCollection;
25
use Geocoder\Model\AdminLevel;
26
use Geocoder\Model\Bounds;
27
use Geocoder\Model\Country;
28
use Geocoder\Query\GeocodeQuery;
29
use Geocoder\Query\ReverseQuery;
30
use Geocoder\Provider\Provider;
31
use Http\Client\HttpClient;
32
use Psr\Http\Message\ResponseInterface;
33
34
/**
35
 * @author William Durand <[email protected]>
36
 */
37
final class MapQuest extends AbstractHttpProvider implements Provider
38
{
39
    const DATA_KEY_ADDRESS = 'address';
40
41
    const KEY_API_KEY = 'key';
42
43
    const KEY_LOCATION = 'location';
44
45
    const KEY_OUT_FORMAT = 'outFormat';
46
47
    const KEY_MAX_RESULTS = 'maxResults';
48
49
    const KEY_THUMB_MAPS = 'thumbMaps';
50
51
    const KEY_INTL_MODE = 'intlMode';
52
53
    const KEY_BOUNDING_BOX = 'boundingBox';
54
55
    const KEY_LAT = 'lat';
56
57
    const KEY_LNG = 'lng';
58
59
    const MODE_5BOX = '5BOX';
60
61
    const OPEN_BASE_URL = 'https://open.mapquestapi.com/geocoding/v1/';
62
63
    const LICENSED_BASE_URL = 'https://www.mapquestapi.com/geocoding/v1/';
64
65
    const GEOCODE_ENDPOINT = 'address';
66
67
    const DEFAULT_GEOCODE_PARAMS = [
68
        self::KEY_LOCATION => '',
69
        self::KEY_OUT_FORMAT => 'json',
70
        self::KEY_API_KEY => '',
71
    ];
72
73
    const DEFAULT_GEOCODE_OPTIONS = [
74
        self::KEY_MAX_RESULTS => 3,
75
        self::KEY_THUMB_MAPS => false,
76
    ];
77
78
    const REVERSE_ENDPOINT = 'reverse';
79
80
    const ADMIN_LEVEL_STATE = 1;
81
82
    const ADMIN_LEVEL_COUNTY = 2;
83
84
    /**
85
     * MapQuest offers two geocoding endpoints one commercial (true) and one open (false)
86
     * More information: http://developer.mapquest.com/web/tools/getting-started/platform/licensed-vs-open.
87
     *
88
     * @var bool
89
     */
90
    private $licensed;
91
92
    /**
93
     * @var bool
94
     */
95
    private $useRoadPosition;
96
97
    /**
98
     * @var string
99
     */
100
    private $apiKey;
101
102
    /**
103
     * @param HttpClient $client          an HTTP adapter
104
     * @param string     $apiKey          an API key
105
     * @param bool       $licensed        true to use MapQuest's licensed endpoints, default is false to use the open endpoints (optional)
106
     * @param bool       $useRoadPosition true to use nearest point on a road for the entrance, false to use map display position
107
     */
108 28
    public function __construct(HttpClient $client, string $apiKey, bool $licensed = false, bool $useRoadPosition = false)
109
    {
110 28
        if (empty($apiKey)) {
111
            throw new InvalidCredentials('No API key provided.');
112
        }
113
114 28
        $this->apiKey = $apiKey;
115 28
        $this->licensed = $licensed;
116 28
        $this->useRoadPosition = $useRoadPosition;
117 28
        parent::__construct($client);
118 28
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123 18
    public function geocodeQuery(GeocodeQuery $query): Collection
124
    {
125 18
        $params = static::DEFAULT_GEOCODE_PARAMS;
126 18
        $params[static::KEY_API_KEY] = $this->apiKey;
127
128 18
        $options = static::DEFAULT_GEOCODE_OPTIONS;
129 18
        $options[static::KEY_MAX_RESULTS] = $query->getLimit();
130
131 18
        $useGetQuery = true;
132
133 18
        $address = $this->extractAddressFromQuery($query);
134 18
        if ($address instanceof Location) {
135 3
            $params[static::KEY_LOCATION] = $this->mapAddressToArray($address);
136 3
            $options[static::KEY_INTL_MODE] = static::MODE_5BOX;
137 3
            $useGetQuery = false;
138
        } else {
139 15
            $addressAsText = $query->getText();
140
141 15
            if (!$addressAsText) {
142
                throw new InvalidArgument('Cannot geocode empty address');
143
            }
144
145
            // This API doesn't handle IPs
146 15
            if (filter_var($addressAsText, FILTER_VALIDATE_IP)) {
147 4
                throw new UnsupportedOperation('The MapQuest provider does not support IP addresses, only street addresses.');
148
            }
149
150 11
            $params[static::KEY_LOCATION] = $addressAsText;
151
        }
152
153 14
        $bounds = $query->getBounds();
154 14
        if ($bounds instanceof Bounds) {
155 1
            $options[static::KEY_BOUNDING_BOX] = $this->mapBoundsToArray($bounds);
156 1
            $useGetQuery = false;
157
        }
158
159 14
        if ($useGetQuery) {
160 11
            $params = $this->addOptionsForGetQuery($params, $options);
161
162 11
            return $this->executeGetQuery(static::GEOCODE_ENDPOINT, $params);
163
        } else {
164 3
            $params = $this->addOptionsForPostQuery($params, $options);
165
166 3
            return $this->executePostQuery(static::GEOCODE_ENDPOINT, $params);
167
        }
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173 9
    public function reverseQuery(ReverseQuery $query): Collection
174
    {
175 9
        $coordinates = $query->getCoordinates();
176 9
        $longitude = $coordinates->getLongitude();
177 9
        $latitude = $coordinates->getLatitude();
178
179
        $params = [
180 9
            static::KEY_API_KEY => $this->apiKey,
181 9
            static::KEY_LAT => $latitude,
182 9
            static::KEY_LNG => $longitude,
183
        ];
184
185 9
        return $this->executeGetQuery(static::REVERSE_ENDPOINT, $params);
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191 10
    public function getName(): string
192
    {
193 10
        return 'map_quest';
194
    }
195
196 18
    private function extractAddressFromQuery(GeocodeQuery $query)
197
    {
198 18
        return $query->getData(static::DATA_KEY_ADDRESS);
199
    }
200
201 23
    private function getUrl($endpoint): string
202
    {
203 23
        if ($this->licensed) {
204
            $baseUrl = static::LICENSED_BASE_URL;
205
        } else {
206 23
            $baseUrl = static::OPEN_BASE_URL;
207
        }
208
209 23
        return $baseUrl.$endpoint;
210
    }
211
212 20
    private function addGetQuery(string $url, array $params): string
213
    {
214 20
        return $url.'?'.http_build_query($params, '', '&', PHP_QUERY_RFC3986);
215
    }
216
217 11
    private function addOptionsForGetQuery(array $params, array $options): array
218
    {
219 11
        foreach ($options as $key => $value) {
220 11
            if (false === $value) {
221 11
                $value = 'false';
222 11
            } elseif (true === $value) {
223
                $value = 'true';
224
            }
225 11
            $params[$key] = $value;
226
        }
227
228 11
        return $params;
229
    }
230
231 3
    private function addOptionsForPostQuery(array $params, array $options): array
232
    {
233 3
        $params['options'] = $options;
234
235 3
        return $params;
236
    }
237
238 3
    private function executePostQuery(string $endpoint, array $params)
239
    {
240 3
        $url = $this->getUrl($endpoint);
241
242 3
        $appKey = $params[static::KEY_API_KEY];
243 3
        unset($params[static::KEY_API_KEY]);
244 3
        $url .= '?key='.$appKey;
245
246 3
        $requestBody = json_encode($params);
247 3
        $request = $this->getMessageFactory()->createRequest('POST', $url, [], $requestBody);
248
249 3
        $response = $this->getHttpClient()->sendRequest($request);
250 3
        $content = $this->parseHttpResponse($response, $url);
251
252 3
        return $this->parseResponseContent($content);
253
    }
254
255
    /**
256
     * @param string $url
0 ignored issues
show
Bug introduced by
There is no parameter named $url. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
257
     *
258
     * @return AddressCollection
259
     */
260 20
    private function executeGetQuery(string $endpoint, array $params): AddressCollection
261
    {
262 20
        $baseUrl = $this->getUrl($endpoint);
263 20
        $url = $this->addGetQuery($baseUrl, $params);
264
265 20
        $content = $this->getUrlContents($url);
266
267 10
        return $this->parseResponseContent($content);
268
    }
269
270 13
    private function parseResponseContent(string $content): AddressCollection
271
    {
272 13
        $json = json_decode($content, true);
273
274 13
        if (!isset($json['results']) || empty($json['results'])) {
275 1
            return new AddressCollection([]);
276
        }
277
278 12
        $locations = $json['results'][0]['locations'];
279
280 12
        if (empty($locations)) {
281
            return new AddressCollection([]);
282
        }
283
284 12
        $results = [];
285 12
        foreach ($locations as $location) {
286 12
            if ($location['street'] || $location['postalCode'] || $location['adminArea5'] || $location['adminArea4'] || $location['adminArea3']) {
287 9
                $admins = [];
288
289 9
                $state = $location['adminArea3'];
290 9
                if ($state) {
291 9
                    $code = null;
292 9
                    if (2 == strlen($state)) {
293 4
                        $code = $state;
294
                    }
295 9
                    $admins[] = [
296 9
                        'name' => $state,
297 9
                        'code' => $code,
298 9
                        'level' => static::ADMIN_LEVEL_STATE,
299
                    ];
300
                }
301
302 9
                if ($location['adminArea4']) {
303 6
                    $admins[] = ['name' => $location['adminArea4'], 'level' => static::ADMIN_LEVEL_COUNTY];
304
                }
305
306 9
                $position = $location['latLng'];
307 9
                if (!$this->useRoadPosition) {
308 9
                    if ($location['displayLatLng']) {
309 9
                        $position = $location['displayLatLng'];
310
                    }
311
                }
312
313 9
                $results[] = Address::createFromArray([
314 9
                    'providedBy' => $this->getName(),
315 9
                    'latitude' => $position['lat'],
316 9
                    'longitude' => $position['lng'],
317 9
                    'streetName' => $location['street'] ?: null,
318 9
                    'locality' => $location['adminArea5'] ?: null,
319 9
                    'subLocality' => $location['adminArea6'] ?: null,
320 9
                    'postalCode' => $location['postalCode'] ?: null,
321 9
                    'adminLevels' => $admins,
322 9
                    'country' => $location['adminArea1'] ?: null,
323 9
                    'countryCode' => $location['adminArea1'] ?: null,
324
                ]);
325
            }
326
        }
327
328 12
        return new AddressCollection($results);
329
    }
330
331 3
    private function mapAddressToArray(Location $address): array
332
    {
333 3
        $location = [];
334
335
        $streetParts = [
336 3
            trim($address->getStreetNumber() ?: ''),
337 3
            trim($address->getStreetName() ?: ''),
338
        ];
339 3
        $street = implode(' ', array_filter($streetParts));
340 3
        if ($street) {
341 1
            $location['street'] = $street;
342
        }
343
344 3
        if ($address->getSubLocality()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $address->getSubLocality() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
345 1
            $location['adminArea6'] = $address->getSubLocality();
346 1
            $location['adminArea6Type'] = 'Neighborhood';
347
        }
348
349 3
        if ($address->getLocality()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $address->getLocality() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
350 3
            $location['adminArea5'] = $address->getLocality();
351 3
            $location['adminArea5Type'] = 'City';
352
        }
353
354
        /** @var AdminLevel $adminLevel */
355 3
        foreach ($address->getAdminLevels() as $adminLevel) {
356 1
            switch ($adminLevel->getLevel()) {
357 1
                case static::ADMIN_LEVEL_STATE:
358 1
                    $state = $adminLevel->getCode();
359 1
                    if (!$state) {
360
                        $state = $adminLevel->getName();
361
                    }
362 1
                    $location['adminArea3'] = $state;
363 1
                    $location['adminArea3Type'] = 'State';
364
365 1
                    break;
366
                case static::ADMIN_LEVEL_COUNTY:
367
                    $county = $adminLevel->getName();
368
                    $location['adminArea4'] = $county;
369
                    $location['adminArea4Type'] = 'County';
370
            }
371
        }
372
373 3
        $country = $address->getCountry();
374 3
        if ($country instanceof Country) {
375 1
            $code = $country->getCode();
376 1
            if (!$code) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $code of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
377
                $code = $country->getName();
378
            }
379 1
            $location['adminArea1'] = $code;
380 1
            $location['adminArea1Type'] = 'Country';
381
        }
382
383 3
        $postalCode = $address->getPostalCode();
384 3
        if ($postalCode) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $postalCode of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
385 1
            $location['postalCode'] = $address->getPostalCode();
386
        }
387
388 3
        return $location;
389
    }
390
391 1
    private function mapBoundsToArray(Bounds $bounds)
392
    {
393
        return [
394 1
            'ul' => [static::KEY_LAT => $bounds->getNorth(), static::KEY_LNG => $bounds->getWest()],
395 1
            'lr' => [static::KEY_LAT => $bounds->getSouth(), static::KEY_LNG => $bounds->getEast()],
396
        ];
397
    }
398
399 3
    protected function parseHttpResponse(ResponseInterface $response, string $url): string
400
    {
401 3
        $statusCode = $response->getStatusCode();
402 3
        if (401 === $statusCode || 403 === $statusCode) {
403
            throw new InvalidCredentials();
404 3
        } elseif (429 === $statusCode) {
405
            throw new QuotaExceeded();
406 3
        } elseif ($statusCode >= 300) {
407
            throw InvalidServerResponse::create($url, $statusCode);
408
        }
409
410 3
        $body = (string) $response->getBody();
411 3
        if (empty($body)) {
412
            throw InvalidServerResponse::emptyResponse($url);
413
        }
414
415 3
        return $body;
416
    }
417
}
418