Completed
Push — master ( db4de5...70a866 )
by Tobias
01:54
created

AlgoliaPlaces::getResultAttribute()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 5
cts 5
cp 1
rs 9.9332
c 0
b 0
f 0
cc 4
nc 5
nop 3
crap 4
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\AlgoliaPlaces;
14
15
use Geocoder\Collection;
16
use Geocoder\Exception\InvalidArgument;
17
use Geocoder\Exception\UnsupportedOperation;
18
use Geocoder\Http\Provider\AbstractHttpProvider;
19
use Geocoder\Model\Address;
20
use Geocoder\Model\AddressBuilder;
21
use Geocoder\Model\AddressCollection;
22
use Geocoder\Provider\Provider;
23
use Geocoder\Query\GeocodeQuery;
24
use Geocoder\Query\ReverseQuery;
25
use Http\Client\HttpClient;
26
use Psr\Http\Message\RequestInterface;
27
28
class AlgoliaPlaces extends AbstractHttpProvider implements Provider
29
{
30
    const TYPE_CITY = 'city';
31
32
    const TYPE_COUNTRY = 'country';
33
34
    const TYPE_ADDRESS = 'address';
35
36
    const TYPE_BUS_STOP = 'busStop';
37
38
    const TYPE_TRAIN_STATION = 'trainStation';
39
40
    const TYPE_TOWN_HALL = 'townhall';
41
42
    const TYPE_AIRPORT = 'airport';
43
44
    /** @var string */
45
    const ENDPOINT_URL_SSL = 'https://places-dsn.algolia.net/1/places/query';
46
47
    /** @var string */
48
    private $apiKey;
49
50
    /** @var string */
51
    private $appId;
52
53
    /** @var GeocodeQuery */
54
    private $query;
55
56
    /** @var HttpClient */
57
    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...
58
59 15
    public function __construct(HttpClient $client, string $apiKey = null, string $appId = null)
60
    {
61 15
        parent::__construct($client);
62
63 15
        $this->apiKey = $apiKey;
64 15
        $this->client = $client;
65 15
        $this->appId = $appId;
66 15
    }
67
68 5
    public function getName(): string
69
    {
70 5
        return 'algolia_places';
71
    }
72
73 14
    public function geocodeQuery(GeocodeQuery $query): Collection
74
    {
75 14
        if (filter_var($query->getText(), FILTER_VALIDATE_IP)) {
76 3
            throw new UnsupportedOperation('The AlgoliaPlaces provider does not support IP addresses, only street addresses.');
77
        }
78
79 11
        $this->query = $query;
80
81 11
        $request = $this->getRequest(self::ENDPOINT_URL_SSL);
82 11
        $jsonParsed = AbstractHttpProvider::getParsedResponse($request);
83 5
        $jsonResponse = json_decode($jsonParsed, true);
84
85 5
        if (is_null($jsonResponse)) {
86
            return new AddressCollection([]);
87
        }
88
89 5
        if ($jsonResponse['degradedQuery']) {
90 1
            return new AddressCollection([]);
91
        }
92 4
        if (0 === $jsonResponse['nbHits']) {
93
            return new AddressCollection([]);
94
        }
95
96 4
        return $this->buildResult($jsonResponse, $query->getLocale());
97
    }
98
99
    public function reverseQuery(ReverseQuery $query): Collection
100
    {
101
        throw new UnsupportedOperation('The AlgoliaPlaces provided does not support reverse geocoding.');
102
    }
103
104
    public function getTypes(): array
105
    {
106
        return [
107
            self::TYPE_CITY,
108
            self::TYPE_COUNTRY,
109
            self::TYPE_ADDRESS,
110
            self::TYPE_BUS_STOP,
111
            self::TYPE_TRAIN_STATION,
112
            self::TYPE_TOWN_HALL,
113
            self::TYPE_AIRPORT,
114
        ];
115
    }
116
117 11
    protected function getRequest(string $url): RequestInterface
118
    {
119 11
        return $this->getMessageFactory()->createRequest(
120 11
            'POST',
121
            $url,
122 11
            $this->buildHeaders(),
123 11
            $this->buildData()
124
        );
125
    }
126
127 11
    private function buildData(): string
128
    {
129 11
        $query = $this->query;
130
        $params = [
131 11
            'query' => $query->getText(),
132
            'aroundLatLngViaIP' => false,
133 11
            'language' => $query->getLocale(),
134 11
            'type' => $this->buildType($query),
135 11
            'countries' => $this->buildCountries($query),
136
        ];
137
138 11
        return json_encode(array_filter($params));
139
    }
140
141 11
    private function buildType(GeocodeQuery $query): string
142
    {
143 11
        $type = $query->getData('type', '');
144
145 11
        if (!empty($type) && !in_array($type, $this->getTypes())) {
146
            throw new InvalidArgument(
147
                sprintf('The type provided to AlgoliaPlace provider must be in `%s`', implode(', ', $this->getTypes()))
148
            );
149
        }
150
151 11
        return $type;
152
    }
153
154 11
    private function buildCountries(GeocodeQuery $query): array
155
    {
156
        return array_map(function (string $country) {
157
            if (2 !== strlen($country)) {
158
                throw new InvalidArgument('The country provided to AlgoliaPlace provider must be an ISO 639-1 code.');
159
            }
160
161
            return strtolower($country); // Country codes MUST be lower-cased
162 11
        }, $query->getData('countries') ?? []);
163
    }
164
165
    /**
166
     * @return array
167
     */
168 11
    private function buildHeaders(): array
169
    {
170 11
        if (empty($this->appId) || empty($this->apiKey)) {
171 10
            return [];
172
        }
173
174
        return [
175 1
            'X-Algolia-Application-Id' => $this->appId,
176 1
            'X-Algolia-API-Key' => $this->apiKey,
177
        ];
178
    }
179
180
    /**
181
     * @param array       $jsonResponse
182
     * @param string|null $locale
183
     *
184
     * @return AddressCollection
185
     */
186 4
    private function buildResult(array $jsonResponse, string $locale = null): AddressCollection
187
    {
188 4
        $results = [];
189
190
        // 1. degradedQuery: checkfor if(degradedQuery) and set results accordingly?
191
        // 2. setStreetNumber($result->locale_name) AlgoliaPlaces does not offer streetnumber
192
        //    precision for the geocoding (with the exception to addresses situated in France)
193
194 4
        foreach ($jsonResponse['hits'] as $result) {
195 4
            $builder = new AddressBuilder($this->getName());
196 4
            $builder->setCoordinates($result['_geoloc']['lat'], $result['_geoloc']['lng']);
197
198 4
            if (isset($result['country'])) {
199 4
                $builder->setCountry($this->getResultAttribute($result, 'country', $locale));
200
            }
201
202 4
            $builder->setCountryCode($result['country_code']);
203
204 4
            if (isset($result['city'])) {
205 4
                $builder->setLocality($this->getResultAttribute($result, 'city', $locale));
206
            }
207 4
            if (isset($result['postcode'])) {
208 4
                $builder->setPostalCode($result['postcode'][0]);
209
            }
210 4
            if (isset($result['locale_name'])) {
211
                $builder->setStreetNumber($result['locale_name']);
212
            }
213 4
            if (isset($result['locale_names']) && isset($result['locale_names'][0])) {
214 2
                $builder->setStreetName($this->getResultAttribute($result, 'locale_names', $locale));
215
            }
216 4
            foreach ($result['administrative'] ?? [] as $i => $adminLevel) {
217 4
                $builder->addAdminLevel($i + 1, $adminLevel[0]);
218
            }
219 4
            $results[] = $builder->build(Address::class);
220
        }
221
222 4
        return new AddressCollection($results);
223
    }
224
225
    /**
226
     * When no locale was set in the query, Algolia will return results for all locales.
227
     * In this case, we return the default locale value
228
     *
229
     * @param array       $result
230
     * @param string      $attribute
231
     * @param string|null $locale
232
     *
233
     * @return string|int|float
234
     */
235 4
    private function getResultAttribute(array $result, string $attribute, string $locale = null)
236
    {
237 4
        if (!is_array($result[$attribute])) {
238 2
            return $result[$attribute];
239
        }
240
241 4
        $value = null !== $locale ? $result[$attribute] : $result[$attribute]['default'];
242
243 4
        return is_array($value) ? $value[0] : $value;
244
    }
245
}
246