Completed
Push — master ( d8a40d...979726 )
by Tobias
02:52
created

AlgoliaPlaces::getRequest()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 6
cts 6
cp 1
rs 9.9666
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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 11
            $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(sprintf('The type provided to AlgoliaPlace provider must be in `%s`', implode(', ', $this->getTypes())));
147
        }
148
149 11
        return $type;
150
    }
151
152 11
    private function buildCountries(GeocodeQuery $query): array
153
    {
154
        return array_map(function (string $country) {
155
            if (2 !== strlen($country)) {
156
                throw new InvalidArgument('The country provided to AlgoliaPlace provider must be an ISO 639-1 code.');
157
            }
158
159
            return strtolower($country); // Country codes MUST be lower-cased
160 11
        }, $query->getData('countries') ?? []);
161
    }
162
163
    /**
164
     * @return array
165
     */
166 11
    private function buildHeaders(): array
167
    {
168 11
        if (empty($this->appId) || empty($this->apiKey)) {
169 10
            return [];
170
        }
171
172
        return [
173 1
            'X-Algolia-Application-Id' => $this->appId,
174 1
            'X-Algolia-API-Key' => $this->apiKey,
175
        ];
176
    }
177
178
    /**
179
     * @param array       $jsonResponse
180
     * @param string|null $locale
181
     *
182
     * @return AddressCollection
183
     */
184 4
    private function buildResult(array $jsonResponse, string $locale = null): AddressCollection
185
    {
186 4
        $results = [];
187
188
        // 1. degradedQuery: checkfor if(degradedQuery) and set results accordingly?
189
        // 2. setStreetNumber($result->locale_name) AlgoliaPlaces does not offer streetnumber
190
        //    precision for the geocoding (with the exception to addresses situated in France)
191
192 4
        foreach ($jsonResponse['hits'] as $result) {
193 4
            $builder = new AddressBuilder($this->getName());
194 4
            $builder->setCoordinates($result['_geoloc']['lat'], $result['_geoloc']['lng']);
195
196 4
            if (isset($result['country'])) {
197 4
                $builder->setCountry($this->getResultAttribute($result, 'country', $locale));
198
            }
199
200 4
            $builder->setCountryCode($result['country_code']);
201
202 4
            if (isset($result['city'])) {
203 4
                $builder->setLocality($this->getResultAttribute($result, 'city', $locale));
204
            }
205 4
            if (isset($result['postcode'])) {
206 4
                $builder->setPostalCode($result['postcode'][0]);
207
            }
208 4
            if (isset($result['locale_name'])) {
209
                $builder->setStreetNumber($result['locale_name']);
210
            }
211 4
            if (isset($result['locale_names']) && isset($result['locale_names'][0])) {
212 2
                $builder->setStreetName($this->getResultAttribute($result, 'locale_names', $locale));
213
            }
214 4
            foreach ($result['administrative'] ?? [] as $i => $adminLevel) {
215 4
                $builder->addAdminLevel($i + 1, $adminLevel[0]);
216
            }
217 4
            $results[] = $builder->build(Address::class);
218
        }
219
220 4
        return new AddressCollection($results);
221
    }
222
223
    /**
224
     * When no locale was set in the query, Algolia will return results for all locales.
225
     * In this case, we return the default locale value
226
     *
227
     * @param array       $result
228
     * @param string      $attribute
229
     * @param string|null $locale
230
     *
231
     * @return string|int|float
232
     */
233 4
    private function getResultAttribute(array $result, string $attribute, string $locale = null)
234
    {
235 4
        if (!is_array($result[$attribute])) {
236 2
            return $result[$attribute];
237
        }
238
239 4
        $value = null !== $locale ? $result[$attribute] : $result[$attribute]['default'];
240
241 4
        return is_array($value) ? $value[0] : $value;
242
    }
243
}
244