From 0f76737f479e99df62699c3938434827febeb9b2 Mon Sep 17 00:00:00 2001 From: Giacomo92 Date: Tue, 24 Feb 2026 00:00:51 +0100 Subject: [PATCH 1/6] feat(here): add v8 (Geocoding & Search API) support with v7 compatibility Add support for HERE Geocoding and Search API (v8), making it the new default while preserving full backward compatibility with the legacy Geocoder REST API (v7, retired December 31, 2023). - createUsingApiKey() now creates a v8 provider; use createV7UsingApiKey() for the deprecated v7 API - Constructor new Here(client, appId, appCode) still creates a v7 provider - Version-branching in geocodeQuery/reverseQuery/executeQuery keeps v7 and v8 code paths cleanly separated - parseV8Response() maps the v8 items[] structure to HereAddress - parseV7Response() extracted from the old inline loop (no logic change) - v7 endpoint constants and helpers preserved with @deprecated tags - HereAddress: typed nullable properties and null guards (null safety) --- src/Provider/Here/Here.php | 350 +++++++++++++++++++++--- src/Provider/Here/Model/HereAddress.php | 20 +- 2 files changed, 327 insertions(+), 43 deletions(-) diff --git a/src/Provider/Here/Here.php b/src/Provider/Here/Here.php index 340fc515e..157e792b6 100644 --- a/src/Provider/Here/Here.php +++ b/src/Provider/Here/Here.php @@ -33,46 +33,94 @@ final class Here extends AbstractHttpProvider implements Provider { /** + * HERE Geocoding & Search API (current, recommended). + */ + public const API_V8 = 'v8'; + + /** + * Legacy HERE Geocoder REST API (retired December 31, 2023). + * + * @deprecated The legacy HERE Geocoder REST API was retired on December 31, 2023. + * Use {@see API_V8} and {@see createUsingApiKey()} instead. + */ + public const API_V7 = 'v7'; + + /** + * HERE Geocoding & Search API geocode endpoint (v8). + * + * @var string + */ + public const GEOCODE_ENDPOINT_URL = 'https://geocode.search.hereapi.com/v1/geocode'; + + /** + * HERE Geocoding & Search API reverse geocode endpoint (v8). + * + * @var string + */ + public const REVERSE_ENDPOINT_URL = 'https://revgeocode.search.hereapi.com/v1/revgeocode'; + + /** + * @deprecated Use {@see GEOCODE_ENDPOINT_URL} with the v8 API instead. + * * @var string */ public const GEOCODE_ENDPOINT_URL_API_KEY = 'https://geocoder.ls.hereapi.com/6.2/geocode.json'; /** + * @deprecated Use {@see GEOCODE_ENDPOINT_URL} with the v8 API instead. + * * @var string */ public const GEOCODE_ENDPOINT_URL_APP_CODE = 'https://geocoder.api.here.com/6.2/geocode.json'; /** + * @deprecated Use {@see GEOCODE_ENDPOINT_URL} with the v8 API instead. + * * @var string */ public const GEOCODE_CIT_ENDPOINT_API_KEY = 'https:/geocoder.sit.ls.hereapi.com/6.2/geocode.json'; /** + * @deprecated Use {@see GEOCODE_ENDPOINT_URL} with the v8 API instead. + * * @var string */ public const GEOCODE_CIT_ENDPOINT_APP_CODE = 'https://geocoder.cit.api.here.com/6.2/geocode.json'; /** + * @deprecated Use {@see REVERSE_ENDPOINT_URL} with the v8 API instead. + * * @var string */ public const REVERSE_ENDPOINT_URL_API_KEY = 'https://reverse.geocoder.ls.hereapi.com/6.2/reversegeocode.json'; /** + * @deprecated Use {@see REVERSE_ENDPOINT_URL} with the v8 API instead. + * * @var string */ public const REVERSE_ENDPOINT_URL_APP_CODE = 'https://reverse.geocoder.api.here.com/6.2/reversegeocode.json'; /** + * @deprecated Use {@see REVERSE_ENDPOINT_URL} with the v8 API instead. + * * @var string */ public const REVERSE_CIT_ENDPOINT_URL_API_KEY = 'https://reverse.geocoder.sit.ls.hereapi.com/6.2/reversegeocode.json'; /** + * @deprecated Use {@see REVERSE_ENDPOINT_URL} with the v8 API instead. + * * @var string */ public const REVERSE_CIT_ENDPOINT_URL_APP_CODE = 'https://reverse.geocoder.cit.api.here.com/6.2/reversegeocode.json'; /** + * Additional data parameters supported by the v7 (legacy) geocode endpoint. + * + * @deprecated These parameters are only available in the legacy HERE Geocoder API (v7), + * which was retired on December 31, 2023. Migrate to the v8 API. + * * @var string[] */ public const GEOCODE_ADDITIONAL_DATA_PARAMS = [ @@ -116,26 +164,59 @@ final class Here extends AbstractHttpProvider implements Provider private $apiKey; /** - * @param ClientInterface $client an HTTP adapter - * @param string $appId an App ID - * @param string $appCode an App code - * @param bool $useCIT use Customer Integration Testing environment (CIT) instead of production + * @var string */ - public function __construct(ClientInterface $client, ?string $appId = null, ?string $appCode = null, bool $useCIT = false) + private $apiVersion; + + /** + * Creates a v7 (legacy) provider instance. For new integrations use {@see createUsingApiKey()}. + * + * @deprecated The legacy HERE Geocoder REST API was retired on December 31, 2023. + * Use {@see createUsingApiKey()} with the v8 Geocoding & Search API instead. + * + * @param ClientInterface $client an HTTP adapter + * @param string $appId a HERE App ID (v7 only) + * @param string $appCode a HERE App Code (v7 only) + * @param bool $useCIT use Customer Integration Testing environment (v7 only) + */ + public function __construct(ClientInterface $client, ?string $appId = null, ?string $appCode = null, bool $useCIT = false, string $apiVersion = self::API_V7) { $this->appId = $appId; $this->appCode = $appCode; $this->useCIT = $useCIT; + $this->apiVersion = $apiVersion; parent::__construct($client); } - public static function createUsingApiKey(ClientInterface $client, string $apiKey, bool $useCIT = false): self + /** + * Create a v8 (HERE Geocoding & Search API) provider using an API Key. + * + * This is the recommended factory method for all new integrations. + * See https://www.here.com/docs/bundle/geocoding-and-search-api-migration-guide/page/migration-geocoder/README.html + * for migration instructions from the legacy v7 API. + */ + public static function createUsingApiKey(ClientInterface $client, string $apiKey): self { - $client = new self($client, null, null, $useCIT); - $client->apiKey = $apiKey; + $instance = new self($client, null, null, false, self::API_V8); + $instance->apiKey = $apiKey; - return $client; + return $instance; + } + + /** + * Create a v7 (legacy HERE Geocoder API) provider using an API Key. + * + * @deprecated The legacy HERE Geocoder REST API was retired on December 31, 2023. + * Migrate to {@see createUsingApiKey()} with the v8 Geocoding & Search API. + * See https://www.here.com/docs/bundle/geocoding-and-search-api-migration-guide/page/migration-geocoder/README.html + */ + public static function createV7UsingApiKey(ClientInterface $client, string $apiKey, bool $useCIT = false): self + { + $instance = new self($client, null, null, $useCIT, self::API_V7); + $instance->apiKey = $apiKey; + + return $instance; } public function geocodeQuery(GeocodeQuery $query): Collection @@ -145,6 +226,69 @@ public function geocodeQuery(GeocodeQuery $query): Collection throw new UnsupportedOperation('The Here provider does not support IP addresses, only street addresses.'); } + if (self::API_V8 === $this->apiVersion) { + return $this->geocodeQueryV8($query); + } + + return $this->geocodeQueryV7($query); + } + + public function reverseQuery(ReverseQuery $query): Collection + { + if (self::API_V8 === $this->apiVersion) { + return $this->reverseQueryV8($query); + } + + return $this->reverseQueryV7($query); + } + + private function geocodeQueryV8(GeocodeQuery $query): Collection + { + $queryParams = [ + 'q' => $query->getText(), + 'limit' => $query->getLimit(), + 'apiKey' => $this->apiKey, + ]; + + // Pass-through for Geocoding & Search API geo filters / sorting reference point. + // See https://www.here.com/docs/bundle/geocoding-and-search-api-v7-api-reference/page/index.html#/paths/~1geocode/get + if (null !== $at = $query->getData('at')) { + $queryParams['at'] = $at; + } + if (null !== $in = $query->getData('in')) { + $queryParams['in'] = $in; + } + if (null !== $types = $query->getData('types')) { + $queryParams['types'] = $types; + } + + $qq = []; + if (null !== $country = $query->getData('country')) { + $qq[] = 'country=' . $country; + } + if (null !== $state = $query->getData('state')) { + $qq[] = 'state=' . $state; + } + if (null !== $county = $query->getData('county')) { + $qq[] = 'county=' . $county; + } + if (null !== $city = $query->getData('city')) { + $qq[] = 'city=' . $city; + } + + if (!empty($qq)) { + $queryParams['qq'] = implode(';', $qq); + } + + if (null !== $query->getLocale()) { + $queryParams['lang'] = $query->getLocale(); + } + + return $this->executeQuery(sprintf('%s?%s', self::GEOCODE_ENDPOINT_URL, http_build_query($queryParams)), $query->getLimit()); + } + + private function geocodeQueryV7(GeocodeQuery $query): Collection + { $queryParams = $this->withApiCredentials([ 'searchtext' => $query->getText(), 'gen' => 9, @@ -174,7 +318,24 @@ public function geocodeQuery(GeocodeQuery $query): Collection return $this->executeQuery(sprintf('%s?%s', $this->getBaseUrl($query), http_build_query($queryParams)), $query->getLimit()); } - public function reverseQuery(ReverseQuery $query): Collection + private function reverseQueryV8(ReverseQuery $query): Collection + { + $coordinates = $query->getCoordinates(); + + $queryParams = [ + 'at' => sprintf('%s,%s', $coordinates->getLatitude(), $coordinates->getLongitude()), + 'limit' => $query->getLimit(), + 'apiKey' => $this->apiKey, + ]; + + if (null !== $query->getLocale()) { + $queryParams['lang'] = $query->getLocale(); + } + + return $this->executeQuery(sprintf('%s?%s', self::REVERSE_ENDPOINT_URL, http_build_query($queryParams)), $query->getLimit()); + } + + private function reverseQueryV7(ReverseQuery $query): Collection { $coordinates = $query->getCoordinates(); @@ -194,6 +355,20 @@ private function executeQuery(string $url, int $limit): Collection $json = json_decode($content, true); + if (self::API_V8 === $this->apiVersion) { + // v8 error format: {"error":"Unauthorized","error_description":"..."} + if (isset($json['error']) && 'Unauthorized' === $json['error']) { + throw new InvalidCredentials('Invalid or missing api key.'); + } + + if (isset($json['items'])) { + return $this->parseV8Response($json['items'], $limit); + } + + return new AddressCollection([]); + } + + // v7 error format: {"type":{"subtype":"InvalidCredentials"}} if (isset($json['type'])) { switch ($json['type']['subtype']) { case 'InvalidInputData': @@ -213,8 +388,111 @@ private function executeQuery(string $url, int $limit): Collection return new AddressCollection([]); } - $locations = $json['Response']['View'][0]['Result']; + return $this->parseV7Response($json['Response']['View'][0]['Result'], $limit); + } + private function parseV8Response(array $items, int $limit): Collection + { + $results = []; + + foreach ($items as $item) { + $builder = new AddressBuilder($this->getName()); + + $position = $item['position']; + $builder->setCoordinates($position['lat'], $position['lng']); + + if (isset($item['mapView'])) { + $mapView = $item['mapView']; + $builder->setBounds($mapView['south'], $mapView['west'], $mapView['north'], $mapView['east']); + } + + $address = $item['address']; + $builder->setStreetNumber($address['houseNumber'] ?? null); + $builder->setStreetName($address['street'] ?? null); + $builder->setPostalCode($address['postalCode'] ?? null); + $builder->setLocality($address['city'] ?? null); + // The Geocoding & Search API may provide both `district` and `subdistrict`. Prefer `district` + // for backward compatibility, but fall back to `subdistrict` when `district` is missing. + $builder->setSubLocality($address['district'] ?? ($address['subdistrict'] ?? null)); + $builder->setCountryCode($address['countryCode'] ?? null); + $builder->setCountry($address['countryName'] ?? null); + + /** @var HereAddress $hereAddress */ + $hereAddress = $builder->build(HereAddress::class); + $hereAddress = $hereAddress->withLocationId($item['id'] ?? null); + $hereAddress = $hereAddress->withLocationType($item['resultType'] ?? null); + $hereAddress = $hereAddress->withLocationName($item['title'] ?? null); + + $additionalData = []; + if (isset($address['label'])) { + $additionalData[] = ['key' => 'Label', 'value' => $address['label']]; + } + if (isset($address['countryName'])) { + $additionalData[] = ['key' => 'CountryName', 'value' => $address['countryName']]; + } + if (isset($address['state'])) { + $additionalData[] = ['key' => 'StateName', 'value' => $address['state']]; + } + if (isset($address['stateCode'])) { + $additionalData[] = ['key' => 'StateCode', 'value' => $address['stateCode']]; + } + if (isset($address['county'])) { + $additionalData[] = ['key' => 'CountyName', 'value' => $address['county']]; + } + if (isset($address['countyCode'])) { + $additionalData[] = ['key' => 'CountyCode', 'value' => $address['countyCode']]; + } + if (isset($address['district'])) { + $additionalData[] = ['key' => 'District', 'value' => $address['district']]; + } + if (isset($address['subdistrict'])) { + $additionalData[] = ['key' => 'Subdistrict', 'value' => $address['subdistrict']]; + } + if (isset($address['streets'])) { + $additionalData[] = ['key' => 'Streets', 'value' => $address['streets']]; + } + if (isset($address['block'])) { + $additionalData[] = ['key' => 'Block', 'value' => $address['block']]; + } + if (isset($address['subblock'])) { + $additionalData[] = ['key' => 'Subblock', 'value' => $address['subblock']]; + } + if (isset($address['building'])) { + $additionalData[] = ['key' => 'Building', 'value' => $address['building']]; + } + if (isset($address['unit'])) { + $additionalData[] = ['key' => 'Unit', 'value' => $address['unit']]; + } + + // Item-level metadata + foreach ( + [ + 'politicalView' => 'PoliticalView', + 'houseNumberType' => 'HouseNumberType', + 'addressBlockType' => 'AddressBlockType', + 'localityType' => 'LocalityType', + 'administrativeAreaType' => 'AdministrativeAreaType', + 'distance' => 'Distance', + ] as $sourceKey => $targetKey + ) { + if (isset($item[$sourceKey])) { + $additionalData[] = ['key' => $targetKey, 'value' => $item[$sourceKey]]; + } + } + + $hereAddress = $hereAddress->withAdditionalData($additionalData); + $results[] = $hereAddress; + + if (count($results) >= $limit) { + break; + } + } + + return new AddressCollection($results); + } + + private function parseV7Response(array $locations, int $limit): Collection + { $results = []; foreach ($locations as $loc) { @@ -245,7 +523,7 @@ private function executeQuery(string $url, int $limit): Collection $address = $builder->build(HereAddress::class); $address = $address->withLocationId($location['LocationId'] ?? null); $address = $address->withLocationType($location['LocationType']); - $address = $address->withAdditionalData(array_merge($additionalData, $extraAdditionalData)); + $address = $address->withAdditionalData(array_merge($additionalData ?? [], $extraAdditionalData)); $address = $address->withShape($location['Shape'] ?? null); $results[] = $address; @@ -262,8 +540,31 @@ public function getName(): string return 'Here'; } + public function getBaseUrl(Query $query): string + { + if (self::API_V8 === $this->apiVersion) { + return ($query instanceof ReverseQuery) ? self::REVERSE_ENDPOINT_URL : self::GEOCODE_ENDPOINT_URL; + } + + $usingApiKey = null !== $this->apiKey; + + if ($query instanceof ReverseQuery) { + if ($this->useCIT) { + return $usingApiKey ? self::REVERSE_CIT_ENDPOINT_URL_API_KEY : self::REVERSE_CIT_ENDPOINT_URL_APP_CODE; + } + + return $usingApiKey ? self::REVERSE_ENDPOINT_URL_API_KEY : self::REVERSE_ENDPOINT_URL_APP_CODE; + } + + if ($this->useCIT) { + return $usingApiKey ? self::GEOCODE_CIT_ENDPOINT_API_KEY : self::GEOCODE_CIT_ENDPOINT_APP_CODE; + } + + return $usingApiKey ? self::GEOCODE_ENDPOINT_URL_API_KEY : self::GEOCODE_ENDPOINT_URL_APP_CODE; + } + /** - * Get serialized additional data param. + * Get serialized additional data param (v7 only). */ private function getAdditionalDataParam(GeocodeQuery $query): string { @@ -281,7 +582,7 @@ private function getAdditionalDataParam(GeocodeQuery $query): string } /** - * Add API credentials to query params. + * Add API credentials to query params (v7 only). * * @param array $queryParams * @@ -306,27 +607,8 @@ private function withApiCredentials(array $queryParams): array return $queryParams; } - public function getBaseUrl(Query $query): string - { - $usingApiKey = null !== $this->apiKey; - - if ($query instanceof ReverseQuery) { - if ($this->useCIT) { - return $usingApiKey ? self::REVERSE_CIT_ENDPOINT_URL_API_KEY : self::REVERSE_CIT_ENDPOINT_URL_APP_CODE; - } - - return $usingApiKey ? self::REVERSE_ENDPOINT_URL_API_KEY : self::REVERSE_ENDPOINT_URL_APP_CODE; - } - - if ($this->useCIT) { - return $usingApiKey ? self::GEOCODE_CIT_ENDPOINT_API_KEY : self::GEOCODE_CIT_ENDPOINT_APP_CODE; - } - - return $usingApiKey ? self::GEOCODE_ENDPOINT_URL_API_KEY : self::GEOCODE_ENDPOINT_URL_APP_CODE; - } - /** - * Serialize the component query parameter. + * Serialize the component query parameter (v7 only). * * @param array $components */ diff --git a/src/Provider/Here/Model/HereAddress.php b/src/Provider/Here/Model/HereAddress.php index d91247255..92ee89d53 100644 --- a/src/Provider/Here/Model/HereAddress.php +++ b/src/Provider/Here/Model/HereAddress.php @@ -22,27 +22,27 @@ final class HereAddress extends Address /** * @var string|null */ - private $locationId; + private ?string $locationId = null; /** * @var string|null */ - private $locationType; + private ?string $locationType = null; /** * @var string|null */ - private $locationName; + private ?string $locationName = null; /** * @var array|null */ - private $additionalData; + private ?array $additionalData = []; /** * @var array|null */ - private $shape; + private ?array $shape = []; /** * @return string|null @@ -107,8 +107,10 @@ public function withAdditionalData(?array $additionalData = null): self { $new = clone $this; - foreach ($additionalData as $data) { - $new = $new->addAdditionalData($data['key'], $data['value']); + if (null !== $additionalData) { + foreach ($additionalData as $data) { + $new = $new->addAdditionalData($data['key'], $data['value']); + } } return $new; @@ -136,7 +138,7 @@ public function getAdditionalDataValue(string $name, mixed $default = null): mix public function hasAdditionalDataValue(string $name): bool { - return array_key_exists($name, $this->additionalData); + return null !== $this->additionalData && array_key_exists($name, $this->additionalData); } /** @@ -174,6 +176,6 @@ public function getShapeValue(string $name, mixed $default = null): mixed public function hasShapeValue(string $name): bool { - return array_key_exists($name, $this->shape); + return null !== $this->shape && array_key_exists($name, $this->shape); } } From 4f596658ddddc4d0b6c325b08b85250020897c56 Mon Sep 17 00:00:00 2001 From: Giacomo92 Date: Tue, 24 Feb 2026 00:00:58 +0100 Subject: [PATCH 2/6] test(here): add v8 unit tests and update v7 tests for explicit version selection - HereTest.php: replace createUsingApiKey() with createV7UsingApiKey() in all v7 real-API tests and getProvider() helper, since createUsingApiKey() now creates a v8 provider by default - HereTest.php: add testDefaultVersionIsV8(), testV7ExplicitSelectionViaConstructor(), and testCreateV7UsingApiKeyFactory() to verify API version routing - HereV8Test.php: new test class covering v8 geocode/reverse mapping, invalid credentials, no-results, IP address rejection, query param pass-through, and all HereAddress field assertions --- src/Provider/Here/Tests/HereTest.php | 37 ++- src/Provider/Here/Tests/HereV8Test.php | 355 +++++++++++++++++++++++++ 2 files changed, 384 insertions(+), 8 deletions(-) create mode 100644 src/Provider/Here/Tests/HereV8Test.php diff --git a/src/Provider/Here/Tests/HereTest.php b/src/Provider/Here/Tests/HereTest.php index 3fc9412b5..e68ffaca1 100644 --- a/src/Provider/Here/Tests/HereTest.php +++ b/src/Provider/Here/Tests/HereTest.php @@ -40,7 +40,7 @@ public function testGeocodeWithRealAddress(): void $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); } - $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); + $provider = Here::createV7UsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); $results = $provider->geocodeQuery(GeocodeQuery::create('10 avenue Gambetta, Paris, France')->withLocale('fr-FR')); @@ -75,7 +75,7 @@ public function testGeocodeWithDefaultAdditionalData(): void $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); } - $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); + $provider = Here::createV7UsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); $results = $provider->geocodeQuery(GeocodeQuery::create('Sant Roc, Santa Coloma de Cervelló, Espanya')->withLocale('ca')); @@ -117,7 +117,7 @@ public function testGeocodeWithAdditionalData(): void $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); } - $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); + $provider = Here::createV7UsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); $results = $provider->geocodeQuery(GeocodeQuery::create('Sant Roc, Santa Coloma de Cervelló, Espanya') ->withData('Country2', 'true') @@ -167,7 +167,7 @@ public function testGeocodeWithExtraFilterCountry(): void $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); } - $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); + $provider = Here::createV7UsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); $queryBarcelonaFromSpain = GeocodeQuery::create('Barcelona')->withData('country', 'ES')->withLocale('ca'); $queryBarcelonaFromVenezuela = GeocodeQuery::create('Barcelona')->withData('country', 'VE')->withLocale('ca'); @@ -202,7 +202,7 @@ public function testGeocodeWithExtraFilterCity(): void $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); } - $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); + $provider = Here::createV7UsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); $queryStreetCity1 = GeocodeQuery::create('Carrer de Barcelona')->withData('city', 'Sant Vicenç dels Horts')->withLocale('ca')->withLimit(1); $queryStreetCity2 = GeocodeQuery::create('Carrer de Barcelona')->withData('city', 'Girona')->withLocale('ca')->withLimit(1); @@ -240,7 +240,7 @@ public function testGeocodeWithExtraFilterCounty(): void $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); } - $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); + $provider = Here::createV7UsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); $queryCityRegion1 = GeocodeQuery::create('Cabanes')->withData('county', 'Girona')->withLocale('ca')->withLimit(1); $queryCityRegion2 = GeocodeQuery::create('Cabanes')->withData('county', 'Castelló')->withLocale('ca')->withLimit(1); @@ -274,7 +274,7 @@ public function testReverseWithRealCoordinates(): void $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); } - $provider = Here::createUsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); + $provider = Here::createV7UsingApiKey($this->getHttpClient($_SERVER['HERE_API_KEY']), $_SERVER['HERE_API_KEY']); $results = $provider->reverseQuery(ReverseQuery::fromCoordinates(48.8632156, 2.3887722)); @@ -358,12 +358,33 @@ public function testGeocodeWithRealIPv6(): void $provider->geocodeQuery(GeocodeQuery::create('::ffff:88.188.221.14')); } + public function testDefaultVersionIsV8(): void + { + $provider = Here::createUsingApiKey($this->getMockedHttpClient(), 'some-api-key'); + $this->assertEquals(Here::GEOCODE_ENDPOINT_URL, $provider->getBaseUrl(GeocodeQuery::create('Paris'))); + $this->assertEquals(Here::REVERSE_ENDPOINT_URL, $provider->getBaseUrl(ReverseQuery::fromCoordinates(0, 0))); + } + + public function testV7ExplicitSelectionViaConstructor(): void + { + $provider = new Here($this->getMockedHttpClient(), 'appId', 'appCode'); + $this->assertEquals(Here::GEOCODE_ENDPOINT_URL_APP_CODE, $provider->getBaseUrl(GeocodeQuery::create('Paris'))); + $this->assertEquals(Here::REVERSE_ENDPOINT_URL_APP_CODE, $provider->getBaseUrl(ReverseQuery::fromCoordinates(0, 0))); + } + + public function testCreateV7UsingApiKeyFactory(): void + { + $provider = Here::createV7UsingApiKey($this->getMockedHttpClient(), 'some-api-key'); + $this->assertEquals(Here::GEOCODE_ENDPOINT_URL_API_KEY, $provider->getBaseUrl(GeocodeQuery::create('Paris'))); + $this->assertEquals(Here::REVERSE_ENDPOINT_URL_API_KEY, $provider->getBaseUrl(ReverseQuery::fromCoordinates(0, 0))); + } + public function getProvider(): Here { if (!isset($_SERVER['HERE_API_KEY'])) { $this->markTestSkipped('You need to configure the HERE_API_KEY value in phpunit.xml'); } - return Here::createUsingApiKey($this->getHttpClient(), $_SERVER['HERE_API_KEY']); + return Here::createV7UsingApiKey($this->getHttpClient(), $_SERVER['HERE_API_KEY']); } } diff --git a/src/Provider/Here/Tests/HereV8Test.php b/src/Provider/Here/Tests/HereV8Test.php new file mode 100644 index 000000000..9666fb889 --- /dev/null +++ b/src/Provider/Here/Tests/HereV8Test.php @@ -0,0 +1,355 @@ +getMockedHttpClient(), 'api-key'); + $this->assertEquals('Here', $provider->getName()); + } + + public function testGetBaseUrl(): void + { + $provider = Here::createUsingApiKey($this->getMockedHttpClient(), 'api-key'); + $this->assertEquals(Here::GEOCODE_ENDPOINT_URL, $provider->getBaseUrl(GeocodeQuery::create('Paris'))); + $this->assertEquals(Here::REVERSE_ENDPOINT_URL, $provider->getBaseUrl(ReverseQuery::fromCoordinates(48.8, 2.3))); + } + + public function testGeocodeWithInvalidData(): void + { + $this->expectException(\Geocoder\Exception\InvalidServerResponse::class); + + $provider = Here::createUsingApiKey($this->getMockedHttpClient(), 'api-key'); + $provider->geocodeQuery(GeocodeQuery::create('foobar')); + } + + public function testGeocodeIpv4(): void + { + $this->expectException(\Geocoder\Exception\UnsupportedOperation::class); + $this->expectExceptionMessage('The Here provider does not support IP addresses, only street addresses.'); + + $provider = Here::createUsingApiKey($this->getMockedHttpClient(), 'api-key'); + $provider->geocodeQuery(GeocodeQuery::create('127.0.0.1')); + } + + public function testGeocodeWithLocalhostIPv6(): void + { + $this->expectException(\Geocoder\Exception\UnsupportedOperation::class); + $this->expectExceptionMessage('The Here provider does not support IP addresses, only street addresses.'); + + $provider = Here::createUsingApiKey($this->getMockedHttpClient(), 'api-key'); + $provider->geocodeQuery(GeocodeQuery::create('::1')); + } + + public function testGeocodeWithRealIPv6(): void + { + $this->expectException(\Geocoder\Exception\UnsupportedOperation::class); + $this->expectExceptionMessage('The Here provider does not support IP addresses, only street addresses.'); + + $provider = Here::createUsingApiKey($this->getMockedHttpClient(), 'api-key'); + $provider->geocodeQuery(GeocodeQuery::create('::ffff:88.188.221.14')); + } + + public function testGeocodeInvalidApiKey(): void + { + $this->expectException(\Geocoder\Exception\InvalidCredentials::class); + $this->expectExceptionMessage('Invalid or missing api key.'); + + $provider = Here::createUsingApiKey( + $this->getMockedHttpClient('{"error":"Unauthorized","error_description":"apiKey invalid"}'), + 'bad-key' + ); + $provider->geocodeQuery(GeocodeQuery::create('New York')); + } + + public function testGeocodeWithNoResults(): void + { + $provider = Here::createUsingApiKey( + $this->getMockedHttpClient('{"items":[]}'), + 'api-key' + ); + + $result = $provider->geocodeQuery(GeocodeQuery::create('jsajhgsdkfjhsfkjhaldkadjaslgldasd')); + $this->assertEmpty($result); + } + + public function testGeocodeMapping(): void + { + $json = <<<'JSON' +{ + "items": [ + { + "title": "10 Downing St, London, SW1A 2AA, United Kingdom", + "id": "here:af:streetsection:some-location-id", + "resultType": "houseNumber", + "houseNumberType": "PA", + "address": { + "label": "10 Downing St, London, SW1A 2AA, United Kingdom", + "countryCode": "GBR", + "countryName": "United Kingdom", + "state": "England", + "county": "Greater London", + "city": "London", + "district": "Westminster", + "street": "Downing St", + "postalCode": "SW1A 2AA", + "houseNumber": "10" + }, + "position": { + "lat": 51.50322, + "lng": -0.12768 + }, + "mapView": { + "west": -0.12955, + "south": 51.50232, + "east": -0.12581, + "north": 51.50412 + } + } + ] +} +JSON; + + $provider = Here::createUsingApiKey($this->getMockedHttpClient($json), 'api-key'); + $results = $provider->geocodeQuery(GeocodeQuery::create('10 Downing St, London, UK')); + + $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $results); + $this->assertCount(1, $results); + + /** @var HereAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(HereAddress::class, $result); + + // Coordinates + $this->assertEqualsWithDelta(51.50322, $result->getCoordinates()->getLatitude(), 0.00001); + $this->assertEqualsWithDelta(-0.12768, $result->getCoordinates()->getLongitude(), 0.00001); + + // Bounds (mapView) + $this->assertNotNull($result->getBounds()); + $this->assertEqualsWithDelta(51.50232, $result->getBounds()->getSouth(), 0.00001); + $this->assertEqualsWithDelta(-0.12955, $result->getBounds()->getWest(), 0.00001); + $this->assertEqualsWithDelta(51.50412, $result->getBounds()->getNorth(), 0.00001); + $this->assertEqualsWithDelta(-0.12581, $result->getBounds()->getEast(), 0.00001); + + // Address fields + $this->assertEquals('10', $result->getStreetNumber()); + $this->assertEquals('Downing St', $result->getStreetName()); + $this->assertEquals('SW1A 2AA', $result->getPostalCode()); + $this->assertEquals('London', $result->getLocality()); + $this->assertEquals('Westminster', $result->getSubLocality()); + $this->assertEquals('GBR', $result->getCountry()->getCode()); + $this->assertEquals('United Kingdom', $result->getCountry()->getName()); + + // HERE-specific fields + $this->assertEquals('here:af:streetsection:some-location-id', $result->getLocationId()); + $this->assertEquals('houseNumber', $result->getLocationType()); + $this->assertEquals('10 Downing St, London, SW1A 2AA, United Kingdom', $result->getLocationName()); + + // Additional data from address + $this->assertEquals('10 Downing St, London, SW1A 2AA, United Kingdom', $result->getAdditionalDataValue('Label')); + $this->assertEquals('United Kingdom', $result->getAdditionalDataValue('CountryName')); + $this->assertEquals('England', $result->getAdditionalDataValue('StateName')); + $this->assertEquals('Greater London', $result->getAdditionalDataValue('CountyName')); + $this->assertEquals('Westminster', $result->getAdditionalDataValue('District')); + + // Item-level metadata + $this->assertEquals('PA', $result->getAdditionalDataValue('HouseNumberType')); + } + + public function testReverseMapping(): void + { + $json = <<<'JSON' +{ + "items": [ + { + "title": "Avenue Gambetta, 75020 Paris, France", + "id": "here:af:streetsection:reverse-id", + "resultType": "street", + "address": { + "label": "Avenue Gambetta, 75020 Paris, France", + "countryCode": "FRA", + "countryName": "France", + "state": "Île-de-France", + "county": "Paris", + "city": "Paris", + "street": "Avenue Gambetta", + "postalCode": "75020" + }, + "position": { + "lat": 48.86322, + "lng": 2.38877 + }, + "mapView": { + "west": 2.38530, + "south": 48.86315, + "east": 2.38883, + "north": 48.86322 + } + } + ] +} +JSON; + + $provider = Here::createUsingApiKey($this->getMockedHttpClient($json), 'api-key'); + $results = $provider->reverseQuery(ReverseQuery::fromCoordinates(48.8632156, 2.3887722)); + + $this->assertInstanceOf(\Geocoder\Model\AddressCollection::class, $results); + $this->assertCount(1, $results); + + /** @var HereAddress $result */ + $result = $results->first(); + $this->assertInstanceOf(HereAddress::class, $result); + + $this->assertEqualsWithDelta(48.86322, $result->getCoordinates()->getLatitude(), 0.00001); + $this->assertEqualsWithDelta(2.38877, $result->getCoordinates()->getLongitude(), 0.00001); + $this->assertEquals('Avenue Gambetta', $result->getStreetName()); + $this->assertEquals('75020', $result->getPostalCode()); + $this->assertEquals('Paris', $result->getLocality()); + $this->assertEquals('FRA', $result->getCountry()->getCode()); + $this->assertEquals('France', $result->getCountry()->getName()); + $this->assertEquals('here:af:streetsection:reverse-id', $result->getLocationId()); + $this->assertEquals('street', $result->getLocationType()); + $this->assertEquals('France', $result->getAdditionalDataValue('CountryName')); + $this->assertEquals('Île-de-France', $result->getAdditionalDataValue('StateName')); + } + + public function testResponseWithoutMapView(): void + { + $json = <<<'JSON' +{ + "items": [ + { + "title": "Paris, Île-de-France, France", + "id": "here:cm:namedplace:12345", + "resultType": "locality", + "address": { + "countryCode": "FRA", + "countryName": "France", + "state": "Île-de-France", + "city": "Paris" + }, + "position": { + "lat": 48.85341, + "lng": 2.3488 + } + } + ] +} +JSON; + + $provider = Here::createUsingApiKey($this->getMockedHttpClient($json), 'api-key'); + $results = $provider->geocodeQuery(GeocodeQuery::create('Paris')); + + $this->assertCount(1, $results); + $result = $results->first(); + + $this->assertEqualsWithDelta(48.85341, $result->getCoordinates()->getLatitude(), 0.00001); + $this->assertNull($result->getBounds()); + $this->assertEquals('Paris', $result->getLocality()); + } + + public function testGeocodeWithStructuredParams(): void + { + $json = '{"items":[{"title":"Barcelona, Catalonia, Spain","id":"here:cm:namedplace:1","resultType":"locality","address":{"countryCode":"ESP","countryName":"Spain","state":"Catalonia","city":"Barcelona"},"position":{"lat":41.38879,"lng":2.15899}}]}'; + + $provider = Here::createUsingApiKey($this->getMockedHttpClient($json), 'api-key'); + + $query = GeocodeQuery::create('Barcelona') + ->withData('country', 'ESP') + ->withData('state', 'Catalonia') + ->withLocale('en'); + + $results = $provider->geocodeQuery($query); + $this->assertCount(1, $results); + + $result = $results->first(); + $this->assertEquals('Barcelona', $result->getLocality()); + $this->assertEquals('Spain', $result->getCountry()->getName()); + } + + public function testApiKeyIsIncludedInGeocodeRequest(): void + { + // The mocked client returns empty items; we just want to confirm no exception is thrown + // and that the v8 code path is taken (apiKey param, not app_id/app_code). + $provider = Here::createUsingApiKey($this->getMockedHttpClient('{"items":[]}'), 'test-api-key'); + $result = $provider->geocodeQuery(GeocodeQuery::create('Paris')); + $this->assertEmpty($result); + } + + public function testApiKeyIsIncludedInReverseRequest(): void + { + $provider = Here::createUsingApiKey($this->getMockedHttpClient('{"items":[]}'), 'test-api-key'); + $result = $provider->reverseQuery(ReverseQuery::fromCoordinates(48.85, 2.35)); + $this->assertEmpty($result); + } + + public function testGeocodeWithAdminLevels(): void + { + $json = <<<'JSON' +{ + "items": [ + { + "title": "Test, Region, Country", + "id": "here:test:1", + "resultType": "houseNumber", + "address": { + "countryCode": "DEU", + "countryName": "Germany", + "state": "Bavaria", + "stateCode": "BY", + "county": "Munich", + "countyCode": "M", + "city": "Munich", + "district": "Maxvorstadt", + "street": "Ludwigstrasse", + "postalCode": "80539", + "houseNumber": "1" + }, + "position": { + "lat": 48.14816, + "lng": 11.5735 + } + } + ] +} +JSON; + + $provider = Here::createUsingApiKey($this->getMockedHttpClient($json), 'api-key'); + $results = $provider->geocodeQuery(GeocodeQuery::create('Ludwigstrasse 1, Munich')); + + $this->assertCount(1, $results); + + /** @var HereAddress $result */ + $result = $results->first(); + $this->assertEquals('DEU', $result->getCountry()->getCode()); + $this->assertEquals('Maxvorstadt', $result->getSubLocality()); + $this->assertEquals('BY', $result->getAdditionalDataValue('StateCode')); + $this->assertEquals('Bavaria', $result->getAdditionalDataValue('StateName')); + $this->assertEquals('Munich', $result->getAdditionalDataValue('CountyName')); + $this->assertEquals('M', $result->getAdditionalDataValue('CountyCode')); + $this->assertEquals('Maxvorstadt', $result->getAdditionalDataValue('District')); + } +} From 1bce558be6db33fa99f259898523bdeeb1f2c1d7 Mon Sep 17 00:00:00 2001 From: Giacomo92 Date: Tue, 24 Feb 2026 00:01:11 +0100 Subject: [PATCH 3/6] test(here): add v8 integration tests with cached response fixtures - IntegrationTest.php: rename createProvider() to use createV7UsingApiKey() so existing v7 cached response tests continue to work unchanged - Add createV8Provider() factory and four new v8 integration tests: testGeocodeQueryV8, testGeocodeQueryWithNoResultsV8, testReverseQueryV8, testReverseQueryWithNoResultsV8 - Rename CIT tests to testGeocodeQueryCITv7 / testReverseQueryCITv7 to clarify they test the v7-only CIT environment - getApiKey() now falls back to 'missing' (was unguarded array access) - Add four v8 cached response fixtures (PHP-serialized HERE GS7 API JSON) --- ...m_1dd988ef910c76b8e01d38d6a97fcc96a8b6b2ed | 1 + ...m_74e648fc2acb458a533e134de4d3f35df478ecc4 | 1 + ...m_6c2c10057f25b1cf6b1c98fcb1ead71d1e42932c | 1 + ...m_93cd97b93a39a1a1a61e446f313c91ad6a6a1d22 | 1 + src/Provider/Here/Tests/IntegrationTest.php | 107 +++++++++++++++++- 5 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 src/Provider/Here/Tests/.cached_responses/geocode.search.hereapi.com_1dd988ef910c76b8e01d38d6a97fcc96a8b6b2ed create mode 100644 src/Provider/Here/Tests/.cached_responses/geocode.search.hereapi.com_74e648fc2acb458a533e134de4d3f35df478ecc4 create mode 100644 src/Provider/Here/Tests/.cached_responses/revgeocode.search.hereapi.com_6c2c10057f25b1cf6b1c98fcb1ead71d1e42932c create mode 100644 src/Provider/Here/Tests/.cached_responses/revgeocode.search.hereapi.com_93cd97b93a39a1a1a61e446f313c91ad6a6a1d22 diff --git a/src/Provider/Here/Tests/.cached_responses/geocode.search.hereapi.com_1dd988ef910c76b8e01d38d6a97fcc96a8b6b2ed b/src/Provider/Here/Tests/.cached_responses/geocode.search.hereapi.com_1dd988ef910c76b8e01d38d6a97fcc96a8b6b2ed new file mode 100644 index 000000000..93a349eab --- /dev/null +++ b/src/Provider/Here/Tests/.cached_responses/geocode.search.hereapi.com_1dd988ef910c76b8e01d38d6a97fcc96a8b6b2ed @@ -0,0 +1 @@ +s:12:"{"items":[]}"; \ No newline at end of file diff --git a/src/Provider/Here/Tests/.cached_responses/geocode.search.hereapi.com_74e648fc2acb458a533e134de4d3f35df478ecc4 b/src/Provider/Here/Tests/.cached_responses/geocode.search.hereapi.com_74e648fc2acb458a533e134de4d3f35df478ecc4 new file mode 100644 index 000000000..76f1be6e9 --- /dev/null +++ b/src/Provider/Here/Tests/.cached_responses/geocode.search.hereapi.com_74e648fc2acb458a533e134de4d3f35df478ecc4 @@ -0,0 +1 @@ +s:550:"{"items":[{"title":"10 Downing St, London, SW1A 2AA, United Kingdom","id":"here:af:streetsection:some-id","resultType":"houseNumber","houseNumberType":"PA","address":{"label":"10 Downing St, London, SW1A 2AA, United Kingdom","countryCode":"GBR","countryName":"United Kingdom","state":"England","county":"Greater London","city":"London","district":"Westminster","street":"Downing St","postalCode":"SW1A 2AA","houseNumber":"10"},"position":{"lat":51.50322,"lng":-0.12768},"mapView":{"west":-0.12955,"south":51.50232,"east":-0.12581,"north":51.50412}}]}"; \ No newline at end of file diff --git a/src/Provider/Here/Tests/.cached_responses/revgeocode.search.hereapi.com_6c2c10057f25b1cf6b1c98fcb1ead71d1e42932c b/src/Provider/Here/Tests/.cached_responses/revgeocode.search.hereapi.com_6c2c10057f25b1cf6b1c98fcb1ead71d1e42932c new file mode 100644 index 000000000..93a349eab --- /dev/null +++ b/src/Provider/Here/Tests/.cached_responses/revgeocode.search.hereapi.com_6c2c10057f25b1cf6b1c98fcb1ead71d1e42932c @@ -0,0 +1 @@ +s:12:"{"items":[]}"; \ No newline at end of file diff --git a/src/Provider/Here/Tests/.cached_responses/revgeocode.search.hereapi.com_93cd97b93a39a1a1a61e446f313c91ad6a6a1d22 b/src/Provider/Here/Tests/.cached_responses/revgeocode.search.hereapi.com_93cd97b93a39a1a1a61e446f313c91ad6a6a1d22 new file mode 100644 index 000000000..bff22f102 --- /dev/null +++ b/src/Provider/Here/Tests/.cached_responses/revgeocode.search.hereapi.com_93cd97b93a39a1a1a61e446f313c91ad6a6a1d22 @@ -0,0 +1 @@ +s:533:"{"items":[{"title":"Pennsylvania Ave NW, Washington, DC 20502, United States","id":"here:af:streetsection:whitehouse-id","resultType":"street","address":{"label":"Pennsylvania Ave NW, Washington, DC 20502, United States","countryCode":"USA","countryName":"United States","state":"District of Columbia","county":"District of Columbia","city":"Washington","street":"Pennsylvania Ave NW","postalCode":"20502"},"position":{"lat":38.89812,"lng":-77.03653},"mapView":{"west":-77.04201,"south":38.89625,"east":-77.02961,"north":38.90241}}]}"; \ No newline at end of file diff --git a/src/Provider/Here/Tests/IntegrationTest.php b/src/Provider/Here/Tests/IntegrationTest.php index 5129e3d77..30ce5222c 100644 --- a/src/Provider/Here/Tests/IntegrationTest.php +++ b/src/Provider/Here/Tests/IntegrationTest.php @@ -33,9 +33,23 @@ class IntegrationTest extends ProviderIntegrationTest protected bool $testIpv6 = false; + /** + * Creates a v7 (legacy) provider for backwards-compatible integration tests. + * + * @deprecated The legacy HERE Geocoder REST API was retired on December 31, 2023. + * New integrations should use {@see createV8Provider()} instead. + */ protected function createProvider(ClientInterface $httpClient, bool $useCIT = false) { - return Here::createUsingApiKey($httpClient, $this->getApiKey(), $useCIT); + return Here::createV7UsingApiKey($httpClient, $this->getApiKey(), $useCIT); + } + + /** + * Creates a v8 (Geocoding & Search API) provider. + */ + protected function createV8Provider(ClientInterface $httpClient): Here + { + return Here::createUsingApiKey($httpClient, $this->getApiKey()); } protected function getCacheDir(): string @@ -66,12 +80,12 @@ private function getCachedHttpClient() protected function getApiKey(): string { - return $_SERVER['HERE_APP_ID']; + return $_SERVER['HERE_API_KEY'] ?? 'missing'; } protected function getAppId(): string { - return $_SERVER['HERE_APP_ID']; + return $_SERVER['HERE_APP_ID'] ?? 'missing'; } /** @@ -79,9 +93,13 @@ protected function getAppId(): string */ protected function getAppCode(): string { - return $_SERVER['HERE_APP_CODE']; + return $_SERVER['HERE_APP_CODE'] ?? 'missing'; } + // ------------------------------------------------------------------------- + // v7 (legacy) integration tests — use v7 cached responses + // ------------------------------------------------------------------------- + public function testGeocodeQuery(): void { if (isset($this->skippedTests[__FUNCTION__])) { @@ -107,7 +125,7 @@ public function testGeocodeQuery(): void } } - public function testGeocodeQueryCIT(): void + public function testGeocodeQueryCITv7(): void { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); @@ -164,7 +182,7 @@ public function testReverseQuery(): void $this->assertWellFormattedResult($result); } - public function testReverseQueryCIT(): void + public function testReverseQueryCITv7(): void { if (isset($this->skippedTests[__FUNCTION__])) { $this->markTestSkipped($this->skippedTests[__FUNCTION__]); @@ -196,6 +214,83 @@ public function testReverseQueryWithNoResults(): void $this->assertEquals(0, $result->count()); } + // ------------------------------------------------------------------------- + // v8 (Geocoding & Search API) integration tests — use v8 cached responses + // ------------------------------------------------------------------------- + + public function testGeocodeQueryV8(): void + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + if (!$this->testAddress) { + $this->markTestSkipped('Geocoding address is not supported by this provider'); + } + + $provider = $this->createV8Provider($this->getCachedHttpClient()); + $query = GeocodeQuery::create('10 Downing St, London, UK')->withLocale('en'); + $result = $provider->geocodeQuery($query); + $this->assertWellFormattedResult($result); + + // Check Downing Street + $location = $result->first(); + $this->assertEqualsWithDelta(51.5033, $location->getCoordinates()->getLatitude(), 0.1, 'Latitude should be in London'); + $this->assertEqualsWithDelta(-0.1276, $location->getCoordinates()->getLongitude(), 0.1, 'Longitude should be in London'); + $this->assertStringContainsString('Downing', $location->getStreetName(), 'Street name should contain "Downing St"'); + + if (null !== $streetNumber = $location->getStreetNumber()) { + $this->assertStringContainsString('10', $streetNumber, 'Street number should contain "10"'); + } + } + + public function testGeocodeQueryWithNoResultsV8(): void + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + if (!$this->testAddress) { + $this->markTestSkipped('Geocoding address is not supported by this provider'); + } + + $provider = $this->createV8Provider($this->getCachedHttpClient()); + $query = GeocodeQuery::create('jsajhgsdkfjhsfkjhaldkadjaslgldasd')->withLocale('en'); + $result = $provider->geocodeQuery($query); + $this->assertWellFormattedResult($result); + $this->assertEquals(0, $result->count()); + } + + public function testReverseQueryV8(): void + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + if (!$this->testReverse) { + $this->markTestSkipped('Reverse geocoding address is not supported by this provider'); + } + + $provider = $this->createV8Provider($this->getCachedHttpClient()); + + // Close to the white house + $result = $provider->reverseQuery(ReverseQuery::fromCoordinates(38.900206, -77.036991)->withLocale('en')); + $this->assertWellFormattedResult($result); + } + + public function testReverseQueryWithNoResultsV8(): void + { + if (isset($this->skippedTests[__FUNCTION__])) { + $this->markTestSkipped($this->skippedTests[__FUNCTION__]); + } + + if (!$this->testReverse) { + $this->markTestSkipped('Reverse geocoding address is not supported by this provider'); + } + + $provider = $this->createV8Provider($this->getCachedHttpClient()); + + $result = $provider->reverseQuery(ReverseQuery::fromCoordinates(0, 0)); + $this->assertEquals(0, $result->count()); + } + /** * Make sure that a result for a Geocoder is well formatted. Be aware that even * a Location with no data may be well formatted. From 68327fd4e818690b39a57b22a7b56eed30d706c7 Mon Sep 17 00:00:00 2001 From: Giacomo92 Date: Tue, 24 Feb 2026 00:01:17 +0100 Subject: [PATCH 4/6] docs(here): document v7/v8 API versions, mark v7 deprecated Rewrite Readme.md with dual-version documentation: - Comparison table of v7 (legacy, retired Dec 31 2023) vs v8 (recommended) - v8 usage with createUsingApiKey() as the primary example - v8 query parameter reference (at, in, types, qq-mapped fields) - v8 response field reference (locationId, locationType, additionalData) - v7 usage examples marked @deprecated with migration warnings - Step-by-step migration guide from v7 to v8 - Links to HERE migration guide and retirement announcement --- src/Provider/Here/Readme.md | 76 ++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/src/Provider/Here/Readme.md b/src/Provider/Here/Readme.md index 808731b58..9917b7c41 100644 --- a/src/Provider/Here/Readme.md +++ b/src/Provider/Here/Readme.md @@ -10,8 +10,19 @@ This is the Here provider from the PHP Geocoder. This is a **READ ONLY** repository. See the [main repo](https://github.com/geocoder-php/Geocoder) for information and documentation. -You can find the [documentation for the provider here](https://developer.here.com/documentation/geocoder/dev_guide/topics/resources.html). +## API Versions +This provider supports two HERE API versions: + +| | v8 (Geocoding & Search API) | v7 (Legacy Geocoder API) | +|---|---|---| +| Status | **Recommended** | **Deprecated** — retired December 31, 2023 | +| Geocode endpoint | `geocode.search.hereapi.com/v1/geocode` | `geocoder.ls.hereapi.com/6.2/geocode.json` | +| Reverse endpoint | `revgeocode.search.hereapi.com/v1/revgeocode` | `reverse.geocoder.ls.hereapi.com/6.2/reversegeocode.json` | +| Authentication | API Key only | API Key or App ID + App Code | + +The v7 HERE Geocoder REST API was retired by HERE on **December 31, 2023**. Migrate to v8 as soon as possible. +See the [HERE migration guide](https://www.here.com/docs/bundle/geocoding-and-search-api-migration-guide/page/migration-geocoder/README.html) for details. ### Install @@ -19,33 +30,78 @@ You can find the [documentation for the provider here](https://developer.here.co composer require geocoder-php/here-provider ``` -## Using +## Using v8 (Recommended) -New applications on the Here platform use the `api_key` authentication method. +New and existing applications should use `createUsingApiKey()`, which targets the v8 Geocoding & Search API: ```php $httpClient = new \Http\Discovery\Psr18Client(); -// You must provide an API key +// Provide your HERE API Key $provider = \Geocoder\Provider\Here\Here::createUsingApiKey($httpClient, 'your-api-key'); -$result = $geocoder->geocodeQuery(GeocodeQuery::create('Buckingham Palace, London')); +$result = $geocoder->geocodeQuery(GeocodeQuery::create('10 Downing St, London, UK')); +``` + +### v8 Query Parameters + +The v8 API supports the following extra parameters via `GeocodeQuery::withData()`: + +| Parameter | Description | +|-----------|-------------| +| `at` | Reference position for result sorting, e.g. `"52.5,13.4"` | +| `in` | Geographic area filter, e.g. `"countryCode:DEU"` | +| `types` | Filter result types, e.g. `"houseNumber,street"` | +| `country` | ISO 3166-1 alpha-3 country code filter (mapped to `qq` param) | +| `state` | State/region filter (mapped to `qq` param) | +| `county` | County filter (mapped to `qq` param) | +| `city` | City filter (mapped to `qq` param) | + +### v8 Response Fields + +In addition to standard Geocoder fields, `HereAddress` provides: + +- `getLocationId()` — unique HERE location ID +- `getLocationType()` — result type (`houseNumber`, `street`, `locality`, `administrativeArea`, etc.) +- `getLocationName()` — formatted title of the result +- `getAdditionalDataValue($name)` — access extra fields such as `Label`, `CountryName`, `StateName`, `CountyName`, `CountyCode`, `StateCode`, `District`, `Subdistrict`, `HouseNumberType`, etc. + +## Using v7 (Deprecated — Retired December 31, 2023) + +> **Warning:** The HERE Geocoder REST API v7 was retired on December 31, 2023. Requests will fail. +> Migrate to v8 using `createUsingApiKey()` above. +> See the [HERE retirement announcement](https://www.here.com/learn/blog/additional-important-guidance-on-here-location-services-end-of-life) for details. + +If you have existing code that uses the legacy API Key authentication: + +```php +$httpClient = new \Http\Discovery\Psr18Client(); + +// @deprecated — Migrate to createUsingApiKey() for the v8 API +$provider = \Geocoder\Provider\Here\Here::createV7UsingApiKey($httpClient, 'your-legacy-api-key'); ``` -If you're using the legacy `app_code` authentication method, use the constructor on the provider like so: +If you're using the legacy `app_id` + `app_code` authentication: ```php $httpClient = new \Http\Discovery\Psr18Client(); -// You must provide both the app_id and app_code +// @deprecated — Migrate to createUsingApiKey() for the v8 API $provider = new \Geocoder\Provider\Here\Here($httpClient, 'app-id', 'app-code'); - -$result = $geocoder->geocodeQuery(GeocodeQuery::create('Buckingham Palace, London')); ``` +## Migrating from v7 to v8 + +1. Replace `new Here($client, $appId, $appCode)` or `createV7UsingApiKey(...)` with `createUsingApiKey($client, $apiKey)`. +2. The response structure changes: `additionalData` values like `CountryName`, `StateName`, `CountyName` are still available via `getAdditionalDataValue()`, but are now sourced from v8 address fields. +3. The `shape` data (v7 `IncludeShapeLevel` parameter) is not available in v8. Remove `withData('IncludeShapeLevel', ...)` from your queries. +4. Replace v7-specific `withData()` keys (`Country2`, `IncludeRoutingInformation`, `IncludeChildPOIs`, etc.) with v8 equivalents where available. + +See the [official migration guide](https://www.here.com/docs/bundle/geocoding-and-search-api-migration-guide/page/migration-geocoder/README.html) for a full parameter mapping. + ### Language parameter -Define the preferred language of address elements in the result. Without a preferred language, the Here geocoder will return results in an official country language or in a regional primary language so that local people will understand. Language code must be provided according to RFC 4647 standard. +Define the preferred language of address elements in the result. Without a preferred language, the HERE geocoder will return results in an official country language or in a regional primary language. Language code must be provided according to RFC 4647 standard. ### Contribute From 7e4eac1b860100766f654bc9e4ca3b2bf058acf4 Mon Sep 17 00:00:00 2001 From: Giacomo92 Date: Tue, 24 Feb 2026 00:01:17 +0100 Subject: [PATCH 5/6] docs(here): document v7/v8 API versions, mark v7 deprecated Rewrite Readme.md with dual-version documentation: - Comparison table of v7 (legacy, retired Dec 31 2023) vs v8 (recommended) - v8 usage with createUsingApiKey() as the primary example - v8 query parameter reference (at, in, types, qq-mapped fields) - v8 response field reference (locationId, locationType, additionalData) - v7 usage examples marked @deprecated with migration warnings - Step-by-step migration guide from v7 to v8 - Links to HERE migration guide and retirement announcement --- src/Provider/Here/Readme.md | 76 ++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 10 deletions(-) diff --git a/src/Provider/Here/Readme.md b/src/Provider/Here/Readme.md index 808731b58..9917b7c41 100644 --- a/src/Provider/Here/Readme.md +++ b/src/Provider/Here/Readme.md @@ -10,8 +10,19 @@ This is the Here provider from the PHP Geocoder. This is a **READ ONLY** repository. See the [main repo](https://github.com/geocoder-php/Geocoder) for information and documentation. -You can find the [documentation for the provider here](https://developer.here.com/documentation/geocoder/dev_guide/topics/resources.html). +## API Versions +This provider supports two HERE API versions: + +| | v8 (Geocoding & Search API) | v7 (Legacy Geocoder API) | +|---|---|---| +| Status | **Recommended** | **Deprecated** — retired December 31, 2023 | +| Geocode endpoint | `geocode.search.hereapi.com/v1/geocode` | `geocoder.ls.hereapi.com/6.2/geocode.json` | +| Reverse endpoint | `revgeocode.search.hereapi.com/v1/revgeocode` | `reverse.geocoder.ls.hereapi.com/6.2/reversegeocode.json` | +| Authentication | API Key only | API Key or App ID + App Code | + +The v7 HERE Geocoder REST API was retired by HERE on **December 31, 2023**. Migrate to v8 as soon as possible. +See the [HERE migration guide](https://www.here.com/docs/bundle/geocoding-and-search-api-migration-guide/page/migration-geocoder/README.html) for details. ### Install @@ -19,33 +30,78 @@ You can find the [documentation for the provider here](https://developer.here.co composer require geocoder-php/here-provider ``` -## Using +## Using v8 (Recommended) -New applications on the Here platform use the `api_key` authentication method. +New and existing applications should use `createUsingApiKey()`, which targets the v8 Geocoding & Search API: ```php $httpClient = new \Http\Discovery\Psr18Client(); -// You must provide an API key +// Provide your HERE API Key $provider = \Geocoder\Provider\Here\Here::createUsingApiKey($httpClient, 'your-api-key'); -$result = $geocoder->geocodeQuery(GeocodeQuery::create('Buckingham Palace, London')); +$result = $geocoder->geocodeQuery(GeocodeQuery::create('10 Downing St, London, UK')); +``` + +### v8 Query Parameters + +The v8 API supports the following extra parameters via `GeocodeQuery::withData()`: + +| Parameter | Description | +|-----------|-------------| +| `at` | Reference position for result sorting, e.g. `"52.5,13.4"` | +| `in` | Geographic area filter, e.g. `"countryCode:DEU"` | +| `types` | Filter result types, e.g. `"houseNumber,street"` | +| `country` | ISO 3166-1 alpha-3 country code filter (mapped to `qq` param) | +| `state` | State/region filter (mapped to `qq` param) | +| `county` | County filter (mapped to `qq` param) | +| `city` | City filter (mapped to `qq` param) | + +### v8 Response Fields + +In addition to standard Geocoder fields, `HereAddress` provides: + +- `getLocationId()` — unique HERE location ID +- `getLocationType()` — result type (`houseNumber`, `street`, `locality`, `administrativeArea`, etc.) +- `getLocationName()` — formatted title of the result +- `getAdditionalDataValue($name)` — access extra fields such as `Label`, `CountryName`, `StateName`, `CountyName`, `CountyCode`, `StateCode`, `District`, `Subdistrict`, `HouseNumberType`, etc. + +## Using v7 (Deprecated — Retired December 31, 2023) + +> **Warning:** The HERE Geocoder REST API v7 was retired on December 31, 2023. Requests will fail. +> Migrate to v8 using `createUsingApiKey()` above. +> See the [HERE retirement announcement](https://www.here.com/learn/blog/additional-important-guidance-on-here-location-services-end-of-life) for details. + +If you have existing code that uses the legacy API Key authentication: + +```php +$httpClient = new \Http\Discovery\Psr18Client(); + +// @deprecated — Migrate to createUsingApiKey() for the v8 API +$provider = \Geocoder\Provider\Here\Here::createV7UsingApiKey($httpClient, 'your-legacy-api-key'); ``` -If you're using the legacy `app_code` authentication method, use the constructor on the provider like so: +If you're using the legacy `app_id` + `app_code` authentication: ```php $httpClient = new \Http\Discovery\Psr18Client(); -// You must provide both the app_id and app_code +// @deprecated — Migrate to createUsingApiKey() for the v8 API $provider = new \Geocoder\Provider\Here\Here($httpClient, 'app-id', 'app-code'); - -$result = $geocoder->geocodeQuery(GeocodeQuery::create('Buckingham Palace, London')); ``` +## Migrating from v7 to v8 + +1. Replace `new Here($client, $appId, $appCode)` or `createV7UsingApiKey(...)` with `createUsingApiKey($client, $apiKey)`. +2. The response structure changes: `additionalData` values like `CountryName`, `StateName`, `CountyName` are still available via `getAdditionalDataValue()`, but are now sourced from v8 address fields. +3. The `shape` data (v7 `IncludeShapeLevel` parameter) is not available in v8. Remove `withData('IncludeShapeLevel', ...)` from your queries. +4. Replace v7-specific `withData()` keys (`Country2`, `IncludeRoutingInformation`, `IncludeChildPOIs`, etc.) with v8 equivalents where available. + +See the [official migration guide](https://www.here.com/docs/bundle/geocoding-and-search-api-migration-guide/page/migration-geocoder/README.html) for a full parameter mapping. + ### Language parameter -Define the preferred language of address elements in the result. Without a preferred language, the Here geocoder will return results in an official country language or in a regional primary language so that local people will understand. Language code must be provided according to RFC 4647 standard. +Define the preferred language of address elements in the result. Without a preferred language, the HERE geocoder will return results in an official country language or in a regional primary language. Language code must be provided according to RFC 4647 standard. ### Contribute From 5ad2b9890d4eb2cb79fc02265fa0ffa19e11f36c Mon Sep 17 00:00:00 2001 From: Giacomo92 Date: Tue, 24 Feb 2026 00:36:51 +0100 Subject: [PATCH 6/6] feat: update docs --- src/Provider/Here/Readme.md | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Provider/Here/Readme.md b/src/Provider/Here/Readme.md index 9917b7c41..7c750f5f6 100644 --- a/src/Provider/Here/Readme.md +++ b/src/Provider/Here/Readme.md @@ -12,16 +12,29 @@ This is the Here provider from the PHP Geocoder. This is a **READ ONLY** reposit ## API Versions -This provider supports two HERE API versions: +This provider supports two HERE API generations: -| | v8 (Geocoding & Search API) | v7 (Legacy Geocoder API) | +| | Geocoding & Search API | Legacy Geocoder REST API | |---|---|---| -| Status | **Recommended** | **Deprecated** — retired December 31, 2023 | -| Geocode endpoint | `geocode.search.hereapi.com/v1/geocode` | `geocoder.ls.hereapi.com/6.2/geocode.json` | -| Reverse endpoint | `revgeocode.search.hereapi.com/v1/revgeocode` | `reverse.geocoder.ls.hereapi.com/6.2/reversegeocode.json` | +| This provider calls it | **"v8"** (new default) | **"v7"** (deprecated) | +| HERE's own name | "Geocoding & Search API v7" / GS7 | "Geocoder REST API" / 6.2 | +| URL path | `/v1/` | `/6.2/` | | Authentication | API Key only | API Key or App ID + App Code | +| Deprecated by HERE | — | **December 31, 2023** | +| Shut down by HERE | — | **July 2025** | +| Removed from this provider | — | **Next major release** | -The v7 HERE Geocoder REST API was retired by HERE on **December 31, 2023**. Migrate to v8 as soon as possible. +> **Note on version naming:** HERE's legacy API uses URL path `/6.2/` and is called the "Geocoder REST +> API". HERE confusingly named its replacement the "Geocoding & Search API **v7**" (also known as GS7). +> To avoid this collision, this provider uses the shorthand **"v8"** for the new Geocoding & Search API +> and **"v7"** for the legacy 6.2 API. + +**Timeline:** +- **December 31, 2023** — HERE deprecated the legacy Geocoder REST API (v7). +- **July 2025** — HERE shut down the v7 endpoints. Live requests to `geocoder.ls.hereapi.com` will fail. +- **Next major release of this provider** — The v7 compatibility code will be removed: `createV7UsingApiKey()`, `new Here($client, $appId, $appCode)`, all `GEOCODE_ENDPOINT_URL_*` / `REVERSE_ENDPOINT_URL_*` v7 constants, and `parseV7Response()`. + +Migrate to v8 as soon as possible. See the [HERE migration guide](https://www.here.com/docs/bundle/geocoding-and-search-api-migration-guide/page/migration-geocoder/README.html) for details. ### Install @@ -66,10 +79,10 @@ In addition to standard Geocoder fields, `HereAddress` provides: - `getLocationName()` — formatted title of the result - `getAdditionalDataValue($name)` — access extra fields such as `Label`, `CountryName`, `StateName`, `CountyName`, `CountyCode`, `StateCode`, `District`, `Subdistrict`, `HouseNumberType`, etc. -## Using v7 (Deprecated — Retired December 31, 2023) +## Using v7 (Deprecated — Shut down July 2025) -> **Warning:** The HERE Geocoder REST API v7 was retired on December 31, 2023. Requests will fail. -> Migrate to v8 using `createUsingApiKey()` above. +> **Warning:** The HERE Geocoder REST API v7 was deprecated December 31, 2023 and shut down in +> July 2025. Requests will fail. Migrate to v8 using `createUsingApiKey()` above. > See the [HERE retirement announcement](https://www.here.com/learn/blog/additional-important-guidance-on-here-location-services-end-of-life) for details. If you have existing code that uses the legacy API Key authentication: