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); } } diff --git a/src/Provider/Here/Readme.md b/src/Provider/Here/Readme.md index 808731b58..7c750f5f6 100644 --- a/src/Provider/Here/Readme.md +++ b/src/Provider/Here/Readme.md @@ -10,8 +10,32 @@ 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 generations: + +| | Geocoding & Search API | Legacy Geocoder REST API | +|---|---|---| +| 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** | + +> **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 @@ -19,33 +43,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 — Shut down July 2025) + +> **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: + +```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 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/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')); + } +} 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.