Code

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