Skip to content

Commit b404347

Browse files
committed
feat: add support for HERE Geocoding & Search API v7 (GS7)
- Implement GS7 geocoding and reverse geocoding endpoints. - Add createUsingApiKey static factory for easier instantiation. - Enhance HereAddress model with locationName and helper methods. - Update documentation and migrate to PHPUnit 10. BREAKING CHANGE: bump minimum PHP version to 8.2
1 parent ee7d548 commit b404347

18 files changed

+692
-69
lines changed

Here.php

Lines changed: 159 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@
3232
*/
3333
final class Here extends AbstractHttpProvider implements Provider
3434
{
35+
/**
36+
* @var string
37+
*/
38+
public const GS7_GEOCODE_ENDPOINT_URL = 'https://geocode.search.hereapi.com/v1/geocode';
39+
40+
/**
41+
* @var string
42+
*/
43+
public const GS7_REVERSE_ENDPOINT_URL = 'https://revgeocode.search.hereapi.com/v1/revgeocode';
44+
3545
/**
3646
* @var string
3747
*/
@@ -96,43 +106,50 @@ final class Here extends AbstractHttpProvider implements Provider
96106
];
97107

98108
/**
99-
* @var string
109+
* @var string|null
100110
*/
101-
private $appId;
111+
private ?string $appId;
102112

103113
/**
104-
* @var string
114+
* @var string|null
105115
*/
106-
private $appCode;
116+
private ?string $appCode;
107117

108118
/**
109119
* @var bool
110120
*/
111-
private $useCIT;
121+
private bool $useCIT;
122+
123+
/**
124+
* @var string|null
125+
*/
126+
private ?string $apiKey = null;
112127

113128
/**
114129
* @var string
115130
*/
116-
private $apiKey;
131+
private string $version;
117132

118133
/**
119134
* @param ClientInterface $client an HTTP adapter
120-
* @param string $appId an App ID
121-
* @param string $appCode an App code
135+
* @param string|null $appId an App ID
136+
* @param string|null $appCode an App code
122137
* @param bool $useCIT use Customer Integration Testing environment (CIT) instead of production
138+
* @param string $version version of the API to use ('6.2' or '7')
123139
*/
124-
public function __construct(ClientInterface $client, ?string $appId = null, ?string $appCode = null, bool $useCIT = false)
140+
public function __construct(ClientInterface $client, ?string $appId = null, ?string $appCode = null, bool $useCIT = false, string $version = '6.2')
125141
{
126142
$this->appId = $appId;
127143
$this->appCode = $appCode;
128144
$this->useCIT = $useCIT;
145+
$this->version = $version;
129146

130147
parent::__construct($client);
131148
}
132149

133-
public static function createUsingApiKey(ClientInterface $client, string $apiKey, bool $useCIT = false): self
150+
public static function createUsingApiKey(ClientInterface $client, string $apiKey, bool $useCIT = false, string $version = '7'): self
134151
{
135-
$client = new self($client, null, null, $useCIT);
152+
$client = new self($client, null, null, $useCIT, $version);
136153
$client->apiKey = $apiKey;
137154

138155
return $client;
@@ -145,6 +162,10 @@ public function geocodeQuery(GeocodeQuery $query): Collection
145162
throw new UnsupportedOperation('The Here provider does not support IP addresses, only street addresses.');
146163
}
147164

165+
if ('7' === $this->version && null !== $this->apiKey) {
166+
return $this->geocodeQueryGS7($query);
167+
}
168+
148169
$queryParams = $this->withApiCredentials([
149170
'searchtext' => $query->getText(),
150171
'gen' => 9,
@@ -174,8 +195,44 @@ public function geocodeQuery(GeocodeQuery $query): Collection
174195
return $this->executeQuery(sprintf('%s?%s', $this->getBaseUrl($query), http_build_query($queryParams)), $query->getLimit());
175196
}
176197

198+
private function geocodeQueryGS7(GeocodeQuery $query): Collection
199+
{
200+
$queryParams = $this->withApiCredentials([
201+
'q' => $query->getText(),
202+
'limit' => $query->getLimit(),
203+
]);
204+
205+
$qq = [];
206+
if (null !== $country = $query->getData('country')) {
207+
$qq[] = 'country='.$country;
208+
}
209+
if (null !== $state = $query->getData('state')) {
210+
$qq[] = 'state='.$state;
211+
}
212+
if (null !== $county = $query->getData('county')) {
213+
$qq[] = 'county='.$county;
214+
}
215+
if (null !== $city = $query->getData('city')) {
216+
$qq[] = 'city='.$city;
217+
}
218+
219+
if (!empty($qq)) {
220+
$queryParams['qq'] = implode(';', $qq);
221+
}
222+
223+
if (null !== $query->getLocale()) {
224+
$queryParams['lang'] = $query->getLocale();
225+
}
226+
227+
return $this->executeQuery(sprintf('%s?%s', $this->getBaseUrl($query), http_build_query($queryParams)), $query->getLimit());
228+
}
229+
177230
public function reverseQuery(ReverseQuery $query): Collection
178231
{
232+
if ('7' === $this->version && null !== $this->apiKey) {
233+
return $this->reverseQueryGS7($query);
234+
}
235+
179236
$coordinates = $query->getCoordinates();
180237

181238
$queryParams = $this->withApiCredentials([
@@ -188,12 +245,34 @@ public function reverseQuery(ReverseQuery $query): Collection
188245
return $this->executeQuery(sprintf('%s?%s', $this->getBaseUrl($query), http_build_query($queryParams)), $query->getLimit());
189246
}
190247

248+
private function reverseQueryGS7(ReverseQuery $query): Collection
249+
{
250+
$coordinates = $query->getCoordinates();
251+
252+
$queryParams = $this->withApiCredentials([
253+
'at' => sprintf('%s,%s', $coordinates->getLatitude(), $coordinates->getLongitude()),
254+
'limit' => $query->getLimit(),
255+
]);
256+
257+
if (null !== $query->getLocale()) {
258+
$queryParams['lang'] = $query->getLocale();
259+
}
260+
261+
return $this->executeQuery(sprintf('%s?%s', $this->getBaseUrl($query), http_build_query($queryParams)), $query->getLimit());
262+
}
263+
191264
private function executeQuery(string $url, int $limit): Collection
192265
{
193266
$content = $this->getUrlContents($url);
194267

195268
$json = json_decode($content, true);
196269

270+
if (isset($json['error'])) {
271+
if ('Unauthorized' === $json['error']) {
272+
throw new InvalidCredentials('Invalid or missing api key.');
273+
}
274+
}
275+
197276
if (isset($json['type'])) {
198277
switch ($json['type']['subtype']) {
199278
case 'InvalidInputData':
@@ -205,6 +284,10 @@ private function executeQuery(string $url, int $limit): Collection
205284
}
206285
}
207286

287+
if (isset($json['items'])) {
288+
return $this->parseGS7Response($json['items'], $limit);
289+
}
290+
208291
if (!isset($json['Response']) || empty($json['Response'])) {
209292
return new AddressCollection([]);
210293
}
@@ -215,6 +298,63 @@ private function executeQuery(string $url, int $limit): Collection
215298

216299
$locations = $json['Response']['View'][0]['Result'];
217300

301+
return $this->parseV6Response($locations, $limit);
302+
}
303+
304+
private function parseGS7Response(array $items, int $limit): Collection
305+
{
306+
$results = [];
307+
308+
foreach ($items as $item) {
309+
$builder = new AddressBuilder($this->getName());
310+
311+
$position = $item['position'];
312+
$builder->setCoordinates($position['lat'], $position['lng']);
313+
314+
if (isset($item['mapView'])) {
315+
$mapView = $item['mapView'];
316+
$builder->setBounds($mapView['south'], $mapView['west'], $mapView['north'], $mapView['east']);
317+
}
318+
319+
$address = $item['address'];
320+
$builder->setStreetNumber($address['houseNumber'] ?? null);
321+
$builder->setStreetName($address['street'] ?? null);
322+
$builder->setPostalCode($address['postalCode'] ?? null);
323+
$builder->setLocality($address['city'] ?? null);
324+
$builder->setSubLocality($address['district'] ?? null);
325+
$builder->setCountryCode($address['countryCode'] ?? null);
326+
$builder->setCountry($address['countryName'] ?? null);
327+
328+
/** @var HereAddress $hereAddress */
329+
$hereAddress = $builder->build(HereAddress::class);
330+
$hereAddress = $hereAddress->withLocationId($item['id'] ?? null);
331+
$hereAddress = $hereAddress->withLocationType($item['resultType'] ?? null);
332+
$hereAddress = $hereAddress->withLocationName($item['title'] ?? null);
333+
334+
$additionalData = [];
335+
if (isset($address['countryName'])) {
336+
$additionalData[] = ['key' => 'CountryName', 'value' => $address['countryName']];
337+
}
338+
if (isset($address['state'])) {
339+
$additionalData[] = ['key' => 'StateName', 'value' => $address['state']];
340+
}
341+
if (isset($address['county'])) {
342+
$additionalData[] = ['key' => 'CountyName', 'value' => $address['county']];
343+
}
344+
345+
$hereAddress = $hereAddress->withAdditionalData($additionalData);
346+
$results[] = $hereAddress;
347+
348+
if (count($results) >= $limit) {
349+
break;
350+
}
351+
}
352+
353+
return new AddressCollection($results);
354+
}
355+
356+
private function parseV6Response(array $locations, int $limit): Collection
357+
{
218358
$results = [];
219359

220360
foreach ($locations as $loc) {
@@ -245,7 +385,7 @@ private function executeQuery(string $url, int $limit): Collection
245385
$address = $builder->build(HereAddress::class);
246386
$address = $address->withLocationId($location['LocationId'] ?? null);
247387
$address = $address->withLocationType($location['LocationType']);
248-
$address = $address->withAdditionalData(array_merge($additionalData, $extraAdditionalData));
388+
$address = $address->withAdditionalData(array_merge($additionalData ?? [], $extraAdditionalData));
249389
$address = $address->withShape($location['Shape'] ?? null);
250390
$results[] = $address;
251391

@@ -289,18 +429,13 @@ private function getAdditionalDataParam(GeocodeQuery $query): string
289429
*/
290430
private function withApiCredentials(array $queryParams): array
291431
{
292-
if (
293-
empty($this->apiKey)
294-
&& (empty($this->appId) || empty($this->appCode))
295-
) {
296-
throw new InvalidCredentials('Invalid or missing api key.');
297-
}
298-
299432
if (null !== $this->apiKey) {
300433
$queryParams['apiKey'] = $this->apiKey;
301-
} else {
434+
} elseif (!empty($this->appId) && !empty($this->appCode)) {
302435
$queryParams['app_id'] = $this->appId;
303436
$queryParams['app_code'] = $this->appCode;
437+
} else {
438+
throw new InvalidCredentials('Invalid or missing api key.');
304439
}
305440

306441
return $queryParams;
@@ -310,6 +445,10 @@ public function getBaseUrl(Query $query): string
310445
{
311446
$usingApiKey = null !== $this->apiKey;
312447

448+
if ('7' === $this->version && $usingApiKey) {
449+
return ($query instanceof ReverseQuery) ? self::GS7_REVERSE_ENDPOINT_URL : self::GS7_GEOCODE_ENDPOINT_URL;
450+
}
451+
313452
if ($query instanceof ReverseQuery) {
314453
if ($this->useCIT) {
315454
return $usingApiKey ? self::REVERSE_CIT_ENDPOINT_URL_API_KEY : self::REVERSE_CIT_ENDPOINT_URL_APP_CODE;

Model/HereAddress.php

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,27 @@ final class HereAddress extends Address
2222
/**
2323
* @var string|null
2424
*/
25-
private $locationId;
25+
private ?string $locationId = null;
2626

2727
/**
2828
* @var string|null
2929
*/
30-
private $locationType;
30+
private ?string $locationType = null;
3131

3232
/**
3333
* @var string|null
3434
*/
35-
private $locationName;
35+
private ?string $locationName = null;
3636

3737
/**
3838
* @var array<string, mixed>|null
3939
*/
40-
private $additionalData;
40+
private ?array $additionalData = [];
4141

4242
/**
4343
* @var array<string, mixed>|null
4444
*/
45-
private $shape;
45+
private ?array $shape = [];
4646

4747
/**
4848
* @return string|null
@@ -107,8 +107,10 @@ public function withAdditionalData(?array $additionalData = null): self
107107
{
108108
$new = clone $this;
109109

110-
foreach ($additionalData as $data) {
111-
$new = $new->addAdditionalData($data['key'], $data['value']);
110+
if (null !== $additionalData) {
111+
foreach ($additionalData as $data) {
112+
$new = $new->addAdditionalData($data['key'], $data['value']);
113+
}
112114
}
113115

114116
return $new;
@@ -136,7 +138,7 @@ public function getAdditionalDataValue(string $name, mixed $default = null): mix
136138

137139
public function hasAdditionalDataValue(string $name): bool
138140
{
139-
return array_key_exists($name, $this->additionalData);
141+
return null !== $this->additionalData && array_key_exists($name, $this->additionalData);
140142
}
141143

142144
/**
@@ -174,6 +176,6 @@ public function getShapeValue(string $name, mixed $default = null): mixed
174176

175177
public function hasShapeValue(string $name): bool
176178
{
177-
return array_key_exists($name, $this->shape);
179+
return null !== $this->shape && array_key_exists($name, $this->shape);
178180
}
179181
}

0 commit comments

Comments
 (0)