Completed
Push — master ( ef8b1f...9eff27 )
by Tobias
02:07
created

OpenCage   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 262
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 97.22%

Importance

Changes 0
Metric Value
wmc 43
lcom 1
cbo 12
dl 0
loc 262
ccs 105
cts 108
cp 0.9722
rs 8.96
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
A geocodeQuery() 0 22 5
A reverseQuery() 0 12 2
A getName() 0 4 1
C executeQuery() 0 61 17
A parseCoordinates() 0 27 2
A parseAdminsLevels() 0 11 4
A parseCountry() 0 10 3
A guessLocality() 0 6 1
A guessStreetName() 0 6 1
A guessSubLocality() 0 6 1
A guessBestComponent() 0 10 4

How to fix   Complexity   

Complex Class

Complex classes like OpenCage 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 OpenCage, 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\OpenCage;
14
15
use Geocoder\Exception\InvalidArgument;
16
use Geocoder\Exception\InvalidCredentials;
17
use Geocoder\Exception\QuotaExceeded;
18
use Geocoder\Exception\UnsupportedOperation;
19
use Geocoder\Collection;
20
use Geocoder\Model\AddressBuilder;
21
use Geocoder\Model\AddressCollection;
22
use Geocoder\Provider\OpenCage\Model\OpenCageAddress;
23
use Geocoder\Query\GeocodeQuery;
24
use Geocoder\Query\ReverseQuery;
25
use Geocoder\Http\Provider\AbstractHttpProvider;
26
use Geocoder\Provider\Provider;
27
use Http\Client\HttpClient;
28
29
/**
30
 * @author mtm <[email protected]>
31
 */
32
final class OpenCage extends AbstractHttpProvider implements Provider
33
{
34
    /**
35
     * @var string
36
     */
37
    const GEOCODE_ENDPOINT_URL = 'https://api.opencagedata.com/geocode/v1/json?key=%s&query=%s&limit=%d&pretty=1';
38
39
    /**
40
     * @var string
41
     */
42
    private $apiKey;
43
44
    /**
45
     * @param HttpClient $client an HTTP adapter
46
     * @param string     $apiKey an API key
47
     */
48 30
    public function __construct(HttpClient $client, string $apiKey)
49
    {
50 30
        if (empty($apiKey)) {
51
            throw new InvalidCredentials('No API key provided.');
52
        }
53
54 30
        $this->apiKey = $apiKey;
55 30
        parent::__construct($client);
56 30
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61 29
    public function geocodeQuery(GeocodeQuery $query): Collection
62
    {
63 29
        $address = $query->getText();
64
65
        // This API doesn't handle IPs
66 29
        if (filter_var($address, FILTER_VALIDATE_IP)) {
67 4
            throw new UnsupportedOperation('The OpenCage provider does not support IP addresses, only street addresses.');
68
        }
69
70 25
        $url = sprintf(self::GEOCODE_ENDPOINT_URL, $this->apiKey, urlencode($address), $query->getLimit());
71 25
        if (null !== $countryCode = $query->getData('countrycode')) {
72 1
            $url = sprintf('%s&countrycode=%s', $url, $countryCode);
73
        }
74 25
        if (null !== $bounds = $query->getBounds()) {
75 1
            $url = sprintf('%s&bounds=%s,%s,%s,%s', $url, $bounds->getWest(), $bounds->getSouth(), $bounds->getEast(), $bounds->getNorth());
76
        }
77 25
        if (null !== $proximity = $query->getData('proximity')) {
78 1
            $url = sprintf('%s&proximity=%s', $url, $proximity);
79
        }
80
81 25
        return $this->executeQuery($url, $query->getLocale());
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87 8
    public function reverseQuery(ReverseQuery $query): Collection
88
    {
89 8
        $coordinates = $query->getCoordinates();
90 8
        $address = sprintf('%f, %f', $coordinates->getLatitude(), $coordinates->getLongitude());
91
92 8
        $geocodeQuery = GeocodeQuery::create($address);
93 8
        if (null !== $locale = $query->getLocale()) {
94 1
            $geocodeQuery = $geocodeQuery->withLocale($query->getLocale());
95
        }
96
97 8
        return $this->geocodeQuery($geocodeQuery);
98
    }
99
100
    /**
101
     * {@inheritdoc}
102
     */
103 12
    public function getName(): string
104
    {
105 12
        return 'opencage';
106
    }
107
108
    /**
109
     * @param string      $url
110
     * @param string|null $locale
111
     *
112
     * @return AddressCollection
113
     *
114
     * @throws \Geocoder\Exception\Exception
115
     */
116 25
    private function executeQuery(string $url, string $locale = null): AddressCollection
117
    {
118 25
        if (null !== $locale) {
119 4
            $url = sprintf('%s&language=%s', $url, $locale);
120
        }
121
122 25
        $content = $this->getUrlContents($url);
123 15
        $json = json_decode($content, true);
124
125
        // https://geocoder.opencagedata.com/api#codes
126 15
        if (isset($json['status'])) {
127 14
            switch ($json['status']['code']) {
128 14
                case 400:
129
                    throw new InvalidArgument('Invalid request (a required parameter is missing).');
130 14
                case 402:
131 1
                    throw new QuotaExceeded('Valid request but quota exceeded.');
132 13
                case 403:
133 1
                    throw new InvalidCredentials('Invalid or missing api key.');
134
            }
135
        }
136
137 13
        if (!isset($json['total_results']) || 0 == $json['total_results']) {
138 2
            return new AddressCollection([]);
139
        }
140
141 11
        $locations = $json['results'];
142
143 11
        if (empty($locations)) {
144
            return new AddressCollection([]);
145
        }
146
147 11
        $results = [];
148 11
        foreach ($locations as $location) {
149 11
            $builder = new AddressBuilder($this->getName());
150 11
            $this->parseCoordinates($builder, $location);
151
152 11
            $components = $location['components'];
153 11
            $annotations = $location['annotations'];
154
155 11
            $this->parseAdminsLevels($builder, $components);
156 11
            $this->parseCountry($builder, $components);
157 11
            $builder->setLocality($this->guessLocality($components));
158 11
            $builder->setSubLocality($this->guessSubLocality($components));
159 11
            $builder->setStreetNumber(isset($components['house_number']) ? $components['house_number'] : null);
160 11
            $builder->setStreetName($this->guessStreetName($components));
161 11
            $builder->setPostalCode(isset($components['postcode']) ? $components['postcode'] : null);
162 11
            $builder->setTimezone(isset($annotations['timezone']['name']) ? $annotations['timezone']['name'] : null);
163
164
            /** @var OpenCageAddress $address */
165 11
            $address = $builder->build(OpenCageAddress::class);
166 11
            $address = $address->withMGRS(isset($annotations['MGRS']) ? $annotations['MGRS'] : null);
167 11
            $address = $address->withMaidenhead(isset($annotations['Maidenhead']) ? $annotations['Maidenhead'] : null);
168 11
            $address = $address->withGeohash(isset($annotations['geohash']) ? $annotations['geohash'] : null);
169 11
            $address = $address->withWhat3words(isset($annotations['what3words'], $annotations['what3words']['words']) ? $annotations['what3words']['words'] : null);
170 11
            $address = $address->withFormattedAddress($location['formatted']);
171
172 11
            $results[] = $address;
173
        }
174
175 11
        return new AddressCollection($results);
176
    }
177
178
    /**
179
     * @param AddressBuilder $builder
180
     * @param array          $location
181
     */
182 11
    private function parseCoordinates(AddressBuilder $builder, array $location)
183
    {
184 11
        $builder->setCoordinates($location['geometry']['lat'], $location['geometry']['lng']);
185
186
        $bounds = [
187 11
            'south' => null,
188
            'west' => null,
189
            'north' => null,
190
            'east' => null,
191
        ];
192
193 11
        if (isset($location['bounds'])) {
194
            $bounds = [
195 11
                'south' => $location['bounds']['southwest']['lat'],
196 11
                'west' => $location['bounds']['southwest']['lng'],
197 11
                'north' => $location['bounds']['northeast']['lat'],
198 11
                'east' => $location['bounds']['northeast']['lng'],
199
            ];
200
        }
201
202 11
        $builder->setBounds(
203 11
            $bounds['south'],
204 11
            $bounds['west'],
205 11
            $bounds['north'],
206 11
            $bounds['east']
207
        );
208 11
    }
209
210
    /**
211
     * @param AddressBuilder $builder
212
     * @param array          $components
213
     */
214 11
    private function parseAdminsLevels(AddressBuilder $builder, array $components)
215
    {
216 11
        if (isset($components['state'])) {
217 11
            $stateCode = isset($components['state_code']) ? $components['state_code'] : null;
218 11
            $builder->addAdminLevel(1, $components['state'], $stateCode);
219
        }
220
221 11
        if (isset($components['county'])) {
222 9
            $builder->addAdminLevel(2, $components['county']);
223
        }
224 11
    }
225
226
    /**
227
     * @param AddressBuilder $builder
228
     * @param array          $components
229
     */
230 11
    private function parseCountry(AddressBuilder $builder, array $components)
231
    {
232 11
        if (isset($components['country'])) {
233 11
            $builder->setCountry($components['country']);
234
        }
235
236 11
        if (isset($components['country_code'])) {
237 11
            $builder->setCountryCode(\strtoupper($components['country_code']));
238
        }
239 11
    }
240
241
    /**
242
     * @param array $components
243
     *
244
     * @return null|string
245
     */
246 11
    protected function guessLocality(array $components)
247
    {
248 11
        $localityKeys = ['city', 'town', 'municipality', 'village', 'hamlet', 'locality', 'croft'];
249
250 11
        return $this->guessBestComponent($components, $localityKeys);
251
    }
252
253
    /**
254
     * @param array $components
255
     *
256
     * @return null|string
257
     */
258 11
    protected function guessStreetName(array $components)
259
    {
260 11
        $streetNameKeys = ['road', 'footway', 'street', 'street_name', 'residential', 'path', 'pedestrian', 'road_reference', 'road_reference_intl'];
261
262 11
        return $this->guessBestComponent($components, $streetNameKeys);
263
    }
264
265
    /**
266
     * @param array $components
267
     *
268
     * @return null|string
269
     */
270 11
    protected function guessSubLocality(array $components)
271
    {
272 11
        $subLocalityKeys = ['neighbourhood', 'suburb', 'city_district', 'district', 'quarter', 'houses', 'subdivision'];
273
274 11
        return $this->guessBestComponent($components, $subLocalityKeys);
275
    }
276
277
    /**
278
     * @param array $components
279
     * @param array $keys
280
     *
281
     * @return null|string
282
     */
283 11
    protected function guessBestComponent(array $components, array $keys)
284
    {
285 11
        foreach ($keys as $key) {
286 11
            if (isset($components[$key]) && !empty($components[$key])) {
287 11
                return $components[$key];
288
            }
289
        }
290
291 9
        return null;
292
    }
293
}
294