Here   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 326
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 132
c 1
b 0
f 0
dl 0
loc 326
rs 9.2
wmc 40

10 Methods

Rating   Name   Duplication   Size   Complexity  
A withApiCredentials() 0 17 5
A __construct() 0 7 1
B getBaseUrl() 0 17 8
A getAdditionalDataParam() 0 13 3
A serializeComponents() 0 5 1
A getName() 0 3 1
B geocodeQuery() 0 34 7
A reverseQuery() 0 12 1
A createUsingApiKey() 0 6 1
C executeQuery() 0 67 12

How to fix   Complexity   

Complex Class

Complex classes like Here 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 Here, 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\Here;
14
15
use Geocoder\Collection;
16
use Geocoder\Exception\InvalidArgument;
17
use Geocoder\Exception\InvalidCredentials;
18
use Geocoder\Exception\QuotaExceeded;
19
use Geocoder\Exception\UnsupportedOperation;
20
use Geocoder\Http\Provider\AbstractHttpProvider;
21
use Geocoder\Model\AddressBuilder;
22
use Geocoder\Model\AddressCollection;
23
use Geocoder\Provider\Here\Model\HereAddress;
24
use Geocoder\Provider\Provider;
25
use Geocoder\Query\Query;
26
use Geocoder\Query\GeocodeQuery;
27
use Geocoder\Query\ReverseQuery;
28
use Http\Client\HttpClient;
29
30
/**
31
 * @author Sébastien Barré <[email protected]>
32
 */
33
final class Here extends AbstractHttpProvider implements Provider
34
{
35
    /**
36
     * @var string
37
     */
38
    const GEOCODE_ENDPOINT_URL_API_KEY = 'https://geocoder.ls.hereapi.com/6.2/geocode.json';
39
40
    /**
41
     * @var string
42
     */
43
    const GEOCODE_ENDPOINT_URL_APP_CODE = 'https://geocoder.api.here.com/6.2/geocode.json';
44
45
    /**
46
     * @var string
47
     */
48
    const GEOCODE_CIT_ENDPOINT_API_KEY = 'https:/geocoder.sit.ls.hereapi.com/6.2/geocode.json';
49
50
    /**
51
     * @var string
52
     */
53
    const GEOCODE_CIT_ENDPOINT_APP_CODE = 'https://geocoder.cit.api.here.com/6.2/geocode.json';
54
55
    /**
56
     * @var string
57
     */
58
    const REVERSE_ENDPOINT_URL_API_KEY = 'https://reverse.geocoder.ls.hereapi.com/6.2/reversegeocode.json';
59
60
    /**
61
     * @var string
62
     */
63
    const REVERSE_ENDPOINT_URL_APP_CODE = 'https://reverse.geocoder.api.here.com/6.2/reversegeocode.json';
64
65
    /**
66
     * @var string
67
     */
68
    const REVERSE_CIT_ENDPOINT_URL_API_KEY = 'https://reverse.geocoder.sit.ls.hereapi.com/6.2/reversegeocode.json';
69
70
    /**
71
     * @var string
72
     */
73
    const REVERSE_CIT_ENDPOINT_URL_APP_CODE = 'https://reverse.geocoder.cit.api.here.com/6.2/reversegeocode.json';
74
75
    /**
76
     * @var array
77
     */
78
    const GEOCODE_ADDITIONAL_DATA_PARAMS = [
79
        'CrossingStreets',
80
        'PreserveUnitDesignators',
81
        'Country2',
82
        'IncludeChildPOIs',
83
        'IncludeRoutingInformation',
84
        'AdditionalAddressProvider',
85
        'HouseNumberMode',
86
        'FlexibleAdminValues',
87
        'IntersectionSnapTolerance',
88
        'AddressRangeSqueezeOffset',
89
        'AddressRangeSqueezeFactor',
90
        'AddressRangeSqueezeOffset',
91
        'IncludeShapeLevel',
92
        'RestrictLevel',
93
        'SuppressStreetType',
94
        'NormalizeNames',
95
        'IncludeMicroPointAddresses',
96
    ];
97
98
    /**
99
     * @var string
100
     */
101
    private $appId;
102
103
    /**
104
     * @var string
105
     */
106
    private $appCode;
107
108
    /**
109
     * @var bool
110
     */
111
    private $useCIT;
112
113
    /**
114
     * @var string
115
     */
116
    private $apiKey;
117
118
    /**
119
     * @param HttpClient $client  an HTTP adapter
120
     * @param string     $appId   an App ID
121
     * @param string     $appCode an App code
122
     * @param bool       $useCIT  use Customer Integration Testing environment (CIT) instead of production
123
     */
124
    public function __construct(HttpClient $client, string $appId = null, string $appCode = null, bool $useCIT = false)
125
    {
126
        $this->appId = $appId;
127
        $this->appCode = $appCode;
128
        $this->useCIT = $useCIT;
129
130
        parent::__construct($client);
131
    }
132
133
    public static function createUsingApiKey(HttpClient $client, string $apiKey, bool $useCIT = false): self
134
    {
135
        $client = new self($client, null, null, $useCIT);
136
        $client->apiKey = $apiKey;
137
138
        return $client;
139
    }
140
141
    /**
142
     * {@inheritdoc}
143
     */
144
    public function geocodeQuery(GeocodeQuery $query): Collection
145
    {
146
        // This API doesn't handle IPs
147
        if (filter_var($query->getText(), FILTER_VALIDATE_IP)) {
148
            throw new UnsupportedOperation('The Here provider does not support IP addresses, only street addresses.');
149
        }
150
151
        $queryParams = $this->withApiCredentials([
152
            'searchtext' => $query->getText(),
153
            'gen' => 9,
154
            'additionaldata' => $this->getAdditionalDataParam($query),
155
        ]);
156
157
        if (null !== $query->getData('country')) {
158
            $queryParams['country'] = $query->getData('country');
159
        }
160
161
        if (null !== $query->getData('state')) {
162
            $queryParams['state'] = $query->getData('state');
163
        }
164
165
        if (null !== $query->getData('county')) {
166
            $queryParams['county'] = $query->getData('county');
167
        }
168
169
        if (null !== $query->getData('city')) {
170
            $queryParams['city'] = $query->getData('city');
171
        }
172
173
        if (null !== $query->getLocale()) {
174
            $queryParams['language'] = $query->getLocale();
175
        }
176
177
        return $this->executeQuery(sprintf('%s?%s', $this->getBaseUrl($query), http_build_query($queryParams)), $query->getLimit());
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183
    public function reverseQuery(ReverseQuery $query): Collection
184
    {
185
        $coordinates = $query->getCoordinates();
186
187
        $queryParams = $this->withApiCredentials([
188
            'gen' => 9,
189
            'mode' => 'retrieveAddresses',
190
            'prox' => sprintf('%s,%s', $coordinates->getLatitude(), $coordinates->getLongitude()),
191
            'maxresults' => $query->getLimit(),
192
        ]);
193
194
        return $this->executeQuery(sprintf('%s?%s', $this->getBaseUrl($query), http_build_query($queryParams)), $query->getLimit());
195
    }
196
197
    /**
198
     * @param string $url
199
     * @param int    $limit
200
     *
201
     * @return Collection
202
     */
203
    private function executeQuery(string $url, int $limit): Collection
204
    {
205
        $content = $this->getUrlContents($url);
206
207
        $json = json_decode($content, true);
208
209
        if (isset($json['type'])) {
210
            switch ($json['type']['subtype']) {
211
                case 'InvalidInputData':
212
                    throw new InvalidArgument('Input parameter validation failed.');
213
                case 'QuotaExceeded':
214
                    throw new QuotaExceeded('Valid request but quota exceeded.');
215
                case 'InvalidCredentials':
216
                    throw new InvalidCredentials('Invalid or missing api key.');
217
            }
218
        }
219
220
        if (!isset($json['Response']) || empty($json['Response'])) {
221
            return new AddressCollection([]);
222
        }
223
224
        if (!isset($json['Response']['View'][0])) {
225
            return new AddressCollection([]);
226
        }
227
228
        $locations = $json['Response']['View'][0]['Result'];
229
230
        $results = [];
231
232
        foreach ($locations as $loc) {
233
            $location = $loc['Location'];
234
            $builder = new AddressBuilder($this->getName());
235
            $coordinates = isset($location['NavigationPosition'][0]) ? $location['NavigationPosition'][0] : $location['DisplayPosition'];
236
            $builder->setCoordinates($coordinates['Latitude'], $coordinates['Longitude']);
237
            $bounds = $location['MapView'];
238
239
            $builder->setBounds($bounds['BottomRight']['Latitude'], $bounds['TopLeft']['Longitude'], $bounds['TopLeft']['Latitude'], $bounds['BottomRight']['Longitude']);
240
            $builder->setStreetNumber($location['Address']['HouseNumber'] ?? null);
241
            $builder->setStreetName($location['Address']['Street'] ?? null);
242
            $builder->setPostalCode($location['Address']['PostalCode'] ?? null);
243
            $builder->setLocality($location['Address']['City'] ?? null);
244
            $builder->setSubLocality($location['Address']['District'] ?? null);
245
            $builder->setCountryCode($location['Address']['Country'] ?? null);
246
247
            // The name of the country can be found in the AdditionalData.
248
            $additionalData = $location['Address']['AdditionalData'] ?? null;
249
            if (!empty($additionalData)) {
250
                $builder->setCountry($additionalData[array_search('CountryName', array_column($additionalData, 'key'))]['value'] ?? null);
251
            }
252
253
            // There may be a second AdditionalData. For example if "IncludeRoutingInformation" parameter is added
254
            $extraAdditionalData = $loc['AdditionalData'] ?? [];
255
256
            /** @var HereAddress $address */
257
            $address = $builder->build(HereAddress::class);
258
            $address = $address->withLocationId($location['LocationId']);
259
            $address = $address->withLocationType($location['LocationType']);
260
            $address = $address->withAdditionalData(array_merge($additionalData, $extraAdditionalData));
261
            $address = $address->withShape($location['Shape'] ?? null);
262
            $results[] = $address;
263
264
            if (count($results) >= $limit) {
265
                break;
266
            }
267
        }
268
269
        return new AddressCollection($results);
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    public function getName(): string
276
    {
277
        return 'Here';
278
    }
279
280
    /**
281
     * Get serialized additional data param.
282
     *
283
     * @param GeocodeQuery $query
284
     *
285
     * @return string
286
     */
287
    private function getAdditionalDataParam(GeocodeQuery $query): string
288
    {
289
        $additionalDataParams = [
290
            'IncludeShapeLevel' => 'country',
291
        ];
292
293
        foreach (self::GEOCODE_ADDITIONAL_DATA_PARAMS as $paramKey) {
294
            if (null !== $query->getData($paramKey)) {
295
                $additionalDataParams[$paramKey] = $query->getData($paramKey);
296
            }
297
        }
298
299
        return $this->serializeComponents($additionalDataParams);
300
    }
301
302
    /**
303
     * Add API credentials to query params.
304
     *
305
     * @param array $queryParams
306
     *
307
     * @return array
308
     */
309
    private function withApiCredentials(array $queryParams): array
310
    {
311
        if (
312
            empty($this->apiKey) &&
313
            (empty($this->appId) || empty($this->appCode))
314
        ) {
315
            throw new InvalidCredentials('Invalid or missing api key.');
316
        }
317
318
        if (null !== $this->apiKey) {
319
            $queryParams['apiKey'] = $this->apiKey;
320
        } else {
321
            $queryParams['app_id'] = $this->appId;
322
            $queryParams['app_code'] = $this->appCode;
323
        }
324
325
        return $queryParams;
326
    }
327
328
    public function getBaseUrl(Query $query): string
329
    {
330
        $usingApiKey = null !== $this->apiKey;
331
332
        if ($query instanceof ReverseQuery) {
333
            if ($this->useCIT) {
334
                return $usingApiKey ? self::REVERSE_CIT_ENDPOINT_URL_API_KEY : self::REVERSE_CIT_ENDPOINT_URL_APP_CODE;
335
            }
336
337
            return $usingApiKey ? self::REVERSE_ENDPOINT_URL_API_KEY : self::REVERSE_ENDPOINT_URL_APP_CODE;
338
        }
339
340
        if ($this->useCIT) {
341
            return $usingApiKey ? self::GEOCODE_CIT_ENDPOINT_API_KEY : self::GEOCODE_CIT_ENDPOINT_APP_CODE;
342
        }
343
344
        return $usingApiKey ? self::GEOCODE_ENDPOINT_URL_API_KEY : self::GEOCODE_ENDPOINT_URL_APP_CODE;
345
    }
346
347
    /**
348
     * Serialize the component query parameter.
349
     *
350
     * @param array $components
351
     *
352
     * @return string
353
     */
354
    private function serializeComponents(array $components): string
355
    {
356
        return implode(';', array_map(function ($name, $value) {
357
            return sprintf('%s,%s', $name, $value);
358
        }, array_keys($components), $components));
359
    }
360
}
361