├── .gitignore ├── .github └── workflows │ └── provider.yml ├── LICENSE ├── composer.json ├── CHANGELOG.md ├── Readme.md ├── Model └── GoogleAddress.php └── GoogleMaps.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | phpunit.xml 4 | -------------------------------------------------------------------------------- /.github/workflows/provider.yml: -------------------------------------------------------------------------------- 1 | name: Provider 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | name: PHP ${{ matrix.php-version }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | php-version: ['8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use PHP ${{ matrix.php-version }} 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php-version }} 23 | extensions: curl 24 | - name: Validate composer.json and composer.lock 25 | run: composer validate --strict 26 | - name: Install dependencies 27 | run: composer update --prefer-stable --prefer-dist --no-progress 28 | - name: Run test suite 29 | run: composer run-script test-ci 30 | - name: Upload Coverage report 31 | run: | 32 | wget https://scrutinizer-ci.com/ocular.phar 33 | php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011 — William Durand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geocoder-php/google-maps-provider", 3 | "type": "library", 4 | "description": "Geocoder GoogleMaps adapter", 5 | "keywords": [], 6 | "homepage": "http://geocoder-php.org/Geocoder/", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "William Durand", 11 | "email": "william.durand1@gmail.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^8.0", 16 | "geocoder-php/common-http": "^4.0", 17 | "willdurand/geocoder": "^4.0|^5.0" 18 | }, 19 | "provide": { 20 | "geocoder-php/provider-implementation": "1.0" 21 | }, 22 | "require-dev": { 23 | "geocoder-php/provider-integration-tests": "^1.6.3", 24 | "php-http/message": "^1.0", 25 | "phpunit/phpunit": "^9.6.11" 26 | }, 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "4.0-dev" 30 | } 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Geocoder\\Provider\\GoogleMaps\\": "" 35 | }, 36 | "exclude-from-classmap": [ 37 | "/Tests/" 38 | ] 39 | }, 40 | "minimum-stability": "dev", 41 | "prefer-stable": true, 42 | "scripts": { 43 | "test": "vendor/bin/phpunit", 44 | "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | The change log describes what is "Added", "Removed", "Changed" or "Fixed" between each release. 4 | 5 | ## 4.8.0 6 | 7 | ### Added 8 | 9 | - Add support for PHP Geocoder 5 10 | 11 | ## 4.7.1 12 | 13 | ### Fixed 14 | 15 | - Fix issue with duplicated SubLocalityLevels 16 | 17 | ## 4.7.0 18 | 19 | ### Added 20 | 21 | - Add support for PHP 8.1 22 | - Add GitHub Actions workflow 23 | 24 | ### Removed 25 | 26 | - Drop support for PHP 7.3 27 | 28 | ### Changed 29 | 30 | - Migrate from PHP-HTTP to PSR-18 client 31 | 32 | ## 4.6.0 33 | 34 | ### Added 35 | 36 | - Add support for PHP 8.0 37 | 38 | ### Removed 39 | 40 | - Drop support for PHP 7.2 41 | 42 | ### Changed 43 | 44 | - Upgrade PHPUnit to version 9 45 | 46 | ## 4.5.0 47 | 48 | ### Added 49 | 50 | - Added `postal_code_suffix` field 51 | 52 | ### Removed 53 | 54 | - Drop support for PHP < 7.2 55 | 56 | ## 4.4.0 57 | 58 | ### Added 59 | 60 | - Added [partial_match](https://developers.google.com/maps/documentation/geocoding/intro#Results) 61 | > `partial_match` indicates that the geocoder did not return an exact match for the original request, though it was able to match part of the requested address. You may wish to examine the original request for misspellings and/or an incomplete address. 62 | 63 | ### Fixed 64 | 65 | - Fix "*Administrative level X is defined twice*" issue 66 | 67 | ## 4.3.0 68 | 69 | ### Added 70 | 71 | - Added [component filtering](https://developers.google.com/maps/documentation/geocoding/intro#ComponentFiltering). 72 | 73 | ## 4.2.0 74 | 75 | ### Added 76 | 77 | - Added the `$channel` constructor parameter. 78 | 79 | ## 4.1.0 80 | 81 | ### Added 82 | 83 | - Support for `SubLocality`. 84 | 85 | ## 4.0.0 86 | 87 | First release of this library. 88 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Google Maps Geocoder provider 2 | [![Build Status](https://travis-ci.org/geocoder-php/google-maps-provider.svg?branch=master)](http://travis-ci.org/geocoder-php/google-maps-provider) 3 | [![Latest Stable Version](https://poser.pugx.org/geocoder-php/google-maps-provider/v/stable)](https://packagist.org/packages/geocoder-php/google-maps-provider) 4 | [![Total Downloads](https://poser.pugx.org/geocoder-php/google-maps-provider/downloads)](https://packagist.org/packages/geocoder-php/google-maps-provider) 5 | [![Monthly Downloads](https://poser.pugx.org/geocoder-php/google-maps-provider/d/monthly.png)](https://packagist.org/packages/geocoder-php/google-maps-provider) 6 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/geocoder-php/google-maps-provider.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/google-maps-provider) 7 | [![Quality Score](https://img.shields.io/scrutinizer/g/geocoder-php/google-maps-provider.svg?style=flat-square)](https://scrutinizer-ci.com/g/geocoder-php/google-maps-provider) 8 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 9 | 10 | This is the Google Maps provider from the PHP Geocoder. This is a **READ ONLY** repository. See the 11 | [main repo](https://github.com/geocoder-php/Geocoder) for information and documentation. 12 | 13 | ## Usage 14 | 15 | ```php 16 | $httpClient = new \Http\Discovery\Psr18Client(); 17 | 18 | // You must provide an API key 19 | $provider = new \Geocoder\Provider\GoogleMaps\GoogleMaps($httpClient, null, 'your-api-key'); 20 | 21 | $result = $geocoder->geocodeQuery(GeocodeQuery::create('Buckingham Palace, London')); 22 | ``` 23 | 24 | All requests require a valid API key, however google does have a [free tier](https://cloud.google.com/maps-platform/pricing/) available. 25 | Please see [this page for information on getting an API key](https://developers.google.com/maps/documentation/geocoding/get-api-key). 26 | 27 | ### Google Maps for Business 28 | 29 | Previously, google offered a "Business" version of their APIs. The service has been deprecated, however existing clients 30 | can use the static `business` method on the provider to create a client: 31 | 32 | ```php 33 | 34 | $httpClient = new \Http\Discovery\Psr18Client(); 35 | 36 | // Client ID is required. Private key is optional. 37 | $provider = \Geocoder\Provider\GoogleMaps\GoogleMaps::business($httpClient, 'your-client-id', 'your-private-key'); 38 | 39 | $result = $geocoder->geocodeQuery(GeocodeQuery::create('Buckingham Palace, London')); 40 | ``` 41 | 42 | ### Install 43 | 44 | ```bash 45 | composer require geocoder-php/google-maps-provider 46 | ``` 47 | 48 | 49 | 50 | ### Contribute 51 | 52 | Contributions are very welcome! Send a pull request to the [main repository](https://github.com/geocoder-php/Geocoder) or 53 | report any issues you find on the [issue tracker](https://github.com/geocoder-php/Geocoder/issues). 54 | -------------------------------------------------------------------------------- /Model/GoogleAddress.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | final class GoogleAddress extends Address 23 | { 24 | /** 25 | * @var string|null 26 | */ 27 | private $id; 28 | 29 | /** 30 | * @var string|null 31 | */ 32 | private $locationType; 33 | 34 | /** 35 | * @var string[] 36 | */ 37 | private $resultType = []; 38 | 39 | /** 40 | * @var string|null 41 | */ 42 | private $formattedAddress; 43 | 44 | /** 45 | * @var string|null 46 | */ 47 | private $streetAddress; 48 | 49 | /** 50 | * @var string|null 51 | */ 52 | private $intersection; 53 | 54 | /** 55 | * @var string|null 56 | */ 57 | private $postalCodeSuffix; 58 | 59 | /** 60 | * @var string|null 61 | */ 62 | private $political; 63 | 64 | /** 65 | * @var string|null 66 | */ 67 | private $colloquialArea; 68 | 69 | /** 70 | * @var string|null 71 | */ 72 | private $ward; 73 | 74 | /** 75 | * @var string|null 76 | */ 77 | private $neighborhood; 78 | 79 | /** 80 | * @var string|null 81 | */ 82 | private $premise; 83 | 84 | /** 85 | * @var string|null 86 | */ 87 | private $subpremise; 88 | 89 | /** 90 | * @var string|null 91 | */ 92 | private $naturalFeature; 93 | 94 | /** 95 | * @var string|null 96 | */ 97 | private $airport; 98 | 99 | /** 100 | * @var string|null 101 | */ 102 | private $park; 103 | 104 | /** 105 | * @var string|null 106 | */ 107 | private $pointOfInterest; 108 | 109 | /** 110 | * @var string|null 111 | */ 112 | private $establishment; 113 | 114 | /** 115 | * @var AdminLevelCollection 116 | */ 117 | private $subLocalityLevels; 118 | 119 | /** 120 | * @var bool 121 | */ 122 | private $partialMatch; 123 | 124 | /** 125 | * @return GoogleAddress 126 | */ 127 | public function withId(?string $id = null) 128 | { 129 | $new = clone $this; 130 | $new->id = $id; 131 | 132 | return $new; 133 | } 134 | 135 | /** 136 | * @see https://developers.google.com/places/place-id 137 | * 138 | * @return string|null 139 | */ 140 | public function getId() 141 | { 142 | return $this->id; 143 | } 144 | 145 | /** 146 | * @return GoogleAddress 147 | */ 148 | public function withLocationType(?string $locationType = null) 149 | { 150 | $new = clone $this; 151 | $new->locationType = $locationType; 152 | 153 | return $new; 154 | } 155 | 156 | /** 157 | * @return string|null 158 | */ 159 | public function getLocationType() 160 | { 161 | return $this->locationType; 162 | } 163 | 164 | /** 165 | * @return string[] 166 | */ 167 | public function getResultType(): array 168 | { 169 | return $this->resultType; 170 | } 171 | 172 | /** 173 | * @param string[] $resultType 174 | * 175 | * @return GoogleAddress 176 | */ 177 | public function withResultType(array $resultType) 178 | { 179 | $new = clone $this; 180 | $new->resultType = $resultType; 181 | 182 | return $new; 183 | } 184 | 185 | /** 186 | * @return string|null 187 | */ 188 | public function getFormattedAddress() 189 | { 190 | return $this->formattedAddress; 191 | } 192 | 193 | /** 194 | * @return GoogleAddress 195 | */ 196 | public function withFormattedAddress(?string $formattedAddress = null) 197 | { 198 | $new = clone $this; 199 | $new->formattedAddress = $formattedAddress; 200 | 201 | return $new; 202 | } 203 | 204 | /** 205 | * @return string|null 206 | */ 207 | public function getAirport() 208 | { 209 | return $this->airport; 210 | } 211 | 212 | /** 213 | * @return GoogleAddress 214 | */ 215 | public function withAirport(?string $airport = null) 216 | { 217 | $new = clone $this; 218 | $new->airport = $airport; 219 | 220 | return $new; 221 | } 222 | 223 | /** 224 | * @return string|null 225 | */ 226 | public function getColloquialArea() 227 | { 228 | return $this->colloquialArea; 229 | } 230 | 231 | /** 232 | * @return GoogleAddress 233 | */ 234 | public function withColloquialArea(?string $colloquialArea = null) 235 | { 236 | $new = clone $this; 237 | $new->colloquialArea = $colloquialArea; 238 | 239 | return $new; 240 | } 241 | 242 | /** 243 | * @return string|null 244 | */ 245 | public function getIntersection() 246 | { 247 | return $this->intersection; 248 | } 249 | 250 | /** 251 | * @return GoogleAddress 252 | */ 253 | public function withIntersection(?string $intersection = null) 254 | { 255 | $new = clone $this; 256 | $new->intersection = $intersection; 257 | 258 | return $new; 259 | } 260 | 261 | /** 262 | * @return string|null 263 | */ 264 | public function getPostalCodeSuffix() 265 | { 266 | return $this->postalCodeSuffix; 267 | } 268 | 269 | /** 270 | * @return GoogleAddress 271 | */ 272 | public function withPostalCodeSuffix(?string $postalCodeSuffix = null) 273 | { 274 | $new = clone $this; 275 | $new->postalCodeSuffix = $postalCodeSuffix; 276 | 277 | return $new; 278 | } 279 | 280 | /** 281 | * @return string|null 282 | */ 283 | public function getNaturalFeature() 284 | { 285 | return $this->naturalFeature; 286 | } 287 | 288 | /** 289 | * @return GoogleAddress 290 | */ 291 | public function withNaturalFeature(?string $naturalFeature = null) 292 | { 293 | $new = clone $this; 294 | $new->naturalFeature = $naturalFeature; 295 | 296 | return $new; 297 | } 298 | 299 | /** 300 | * @return string|null 301 | */ 302 | public function getNeighborhood() 303 | { 304 | return $this->neighborhood; 305 | } 306 | 307 | /** 308 | * @return GoogleAddress 309 | */ 310 | public function withNeighborhood(?string $neighborhood = null) 311 | { 312 | $new = clone $this; 313 | $new->neighborhood = $neighborhood; 314 | 315 | return $new; 316 | } 317 | 318 | /** 319 | * @return string|null 320 | */ 321 | public function getPark() 322 | { 323 | return $this->park; 324 | } 325 | 326 | /** 327 | * @return GoogleAddress 328 | */ 329 | public function withPark(?string $park = null) 330 | { 331 | $new = clone $this; 332 | $new->park = $park; 333 | 334 | return $new; 335 | } 336 | 337 | /** 338 | * @return string|null 339 | */ 340 | public function getPointOfInterest() 341 | { 342 | return $this->pointOfInterest; 343 | } 344 | 345 | /** 346 | * @return GoogleAddress 347 | */ 348 | public function withPointOfInterest(?string $pointOfInterest = null) 349 | { 350 | $new = clone $this; 351 | $new->pointOfInterest = $pointOfInterest; 352 | 353 | return $new; 354 | } 355 | 356 | /** 357 | * @return string|null 358 | */ 359 | public function getPolitical() 360 | { 361 | return $this->political; 362 | } 363 | 364 | /** 365 | * @return GoogleAddress 366 | */ 367 | public function withPolitical(?string $political = null) 368 | { 369 | $new = clone $this; 370 | $new->political = $political; 371 | 372 | return $new; 373 | } 374 | 375 | /** 376 | * @return string|null 377 | */ 378 | public function getPremise() 379 | { 380 | return $this->premise; 381 | } 382 | 383 | /** 384 | * @return GoogleAddress 385 | */ 386 | public function withPremise(?string $premise = null) 387 | { 388 | $new = clone $this; 389 | $new->premise = $premise; 390 | 391 | return $new; 392 | } 393 | 394 | /** 395 | * @return string|null 396 | */ 397 | public function getStreetAddress() 398 | { 399 | return $this->streetAddress; 400 | } 401 | 402 | /** 403 | * @return GoogleAddress 404 | */ 405 | public function withStreetAddress(?string $streetAddress = null) 406 | { 407 | $new = clone $this; 408 | $new->streetAddress = $streetAddress; 409 | 410 | return $new; 411 | } 412 | 413 | /** 414 | * @return string|null 415 | */ 416 | public function getSubpremise() 417 | { 418 | return $this->subpremise; 419 | } 420 | 421 | /** 422 | * @return GoogleAddress 423 | */ 424 | public function withSubpremise(?string $subpremise = null) 425 | { 426 | $new = clone $this; 427 | $new->subpremise = $subpremise; 428 | 429 | return $new; 430 | } 431 | 432 | /** 433 | * @return string|null 434 | */ 435 | public function getWard() 436 | { 437 | return $this->ward; 438 | } 439 | 440 | /** 441 | * @return GoogleAddress 442 | */ 443 | public function withWard(?string $ward = null) 444 | { 445 | $new = clone $this; 446 | $new->ward = $ward; 447 | 448 | return $new; 449 | } 450 | 451 | /** 452 | * @return string|null 453 | */ 454 | public function getEstablishment() 455 | { 456 | return $this->establishment; 457 | } 458 | 459 | /** 460 | * @return GoogleAddress 461 | */ 462 | public function withEstablishment(?string $establishment = null) 463 | { 464 | $new = clone $this; 465 | $new->establishment = $establishment; 466 | 467 | return $new; 468 | } 469 | 470 | /** 471 | * @return AdminLevelCollection 472 | */ 473 | public function getSubLocalityLevels() 474 | { 475 | return $this->subLocalityLevels; 476 | } 477 | 478 | /** 479 | * @param array $subLocalityLevel 480 | * 481 | * @return $this 482 | */ 483 | public function withSubLocalityLevels(array $subLocalityLevel) 484 | { 485 | $levels = array_filter($subLocalityLevel, function ($level) { 486 | return !empty($level['level']) && (!empty($level['name']) || !empty($level['code'])); 487 | }); 488 | 489 | $levelCount = array_count_values(array_column($levels, 'level')); 490 | 491 | $subLocalityLevels = []; 492 | foreach ($levelCount as $level => $count) { 493 | $_levels = array_filter($levels, function ($l) use ($level) { 494 | return $l['level'] === $level; 495 | }); 496 | 497 | $names = array_filter(array_column($_levels, 'name'), function ($name) { return !empty($name); }); 498 | $codes = array_filter(array_column($_levels, 'code'), function ($code) { return !empty($code); }); 499 | 500 | $name = count($names) > 0 ? implode(' / ', $names) : implode(' / ', $codes); 501 | $code = count($codes) > 0 ? implode(' / ', $codes) : null; 502 | 503 | $subLocalityLevels[] = new AdminLevel($level, $name, $code); 504 | } 505 | 506 | $new = clone $this; 507 | $new->subLocalityLevels = new AdminLevelCollection($subLocalityLevels); 508 | 509 | return $new; 510 | } 511 | 512 | /** 513 | * @return bool 514 | */ 515 | public function isPartialMatch() 516 | { 517 | return $this->partialMatch; 518 | } 519 | 520 | /** 521 | * @return $this 522 | */ 523 | public function withPartialMatch(bool $partialMatch) 524 | { 525 | $new = clone $this; 526 | $new->partialMatch = $partialMatch; 527 | 528 | return $new; 529 | } 530 | } 531 | -------------------------------------------------------------------------------- /GoogleMaps.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | final class GoogleMaps extends AbstractHttpProvider implements Provider 33 | { 34 | /** 35 | * @var string 36 | */ 37 | public const GEOCODE_ENDPOINT_URL_SSL = 'https://maps.googleapis.com/maps/api/geocode/json?address=%s'; 38 | 39 | /** 40 | * @var string 41 | */ 42 | public const REVERSE_ENDPOINT_URL_SSL = 'https://maps.googleapis.com/maps/api/geocode/json?latlng=%F,%F'; 43 | 44 | /** 45 | * @var string|null 46 | */ 47 | private $region; 48 | 49 | /** 50 | * @var string|null 51 | */ 52 | private $apiKey; 53 | 54 | /** 55 | * @var string|null 56 | */ 57 | private $clientId; 58 | 59 | /** 60 | * @var string|null 61 | */ 62 | private $privateKey; 63 | 64 | /** 65 | * @var string|null 66 | */ 67 | private $channel; 68 | 69 | /** 70 | * Google Maps for Business 71 | * https://developers.google.com/maps/documentation/business/ 72 | * Maps for Business is no longer accepting new signups. 73 | * 74 | * @param ClientInterface $client An HTTP adapter 75 | * @param string $clientId Your Client ID 76 | * @param string $privateKey Your Private Key (optional) 77 | * @param string $region Region biasing (optional) 78 | * @param string $apiKey Google Geocoding API key (optional) 79 | * @param string $channel Google Channel parameter (optional) 80 | * 81 | * @return GoogleMaps 82 | */ 83 | public static function business( 84 | ClientInterface $client, 85 | string $clientId, 86 | ?string $privateKey = null, 87 | ?string $region = null, 88 | ?string $apiKey = null, 89 | ?string $channel = null, 90 | ) { 91 | $provider = new self($client, $region, $apiKey); 92 | $provider->clientId = $clientId; 93 | $provider->privateKey = $privateKey; 94 | $provider->channel = $channel; 95 | 96 | return $provider; 97 | } 98 | 99 | /** 100 | * @param ClientInterface $client An HTTP adapter 101 | * @param string $region Region biasing (optional) 102 | * @param string $apiKey Google Geocoding API key (optional) 103 | */ 104 | public function __construct(ClientInterface $client, ?string $region = null, ?string $apiKey = null) 105 | { 106 | parent::__construct($client); 107 | 108 | $this->region = $region; 109 | $this->apiKey = $apiKey; 110 | } 111 | 112 | public function geocodeQuery(GeocodeQuery $query): Collection 113 | { 114 | // Google API returns invalid data if IP address given 115 | // This API doesn't handle IPs 116 | if (filter_var($query->getText(), FILTER_VALIDATE_IP)) { 117 | throw new UnsupportedOperation('The GoogleMaps provider does not support IP addresses, only street addresses.'); 118 | } 119 | 120 | $url = sprintf(self::GEOCODE_ENDPOINT_URL_SSL, rawurlencode($query->getText())); 121 | if (null !== $bounds = $query->getBounds()) { 122 | $url .= sprintf( 123 | '&bounds=%s,%s|%s,%s', 124 | $bounds->getSouth(), 125 | $bounds->getWest(), 126 | $bounds->getNorth(), 127 | $bounds->getEast() 128 | ); 129 | } 130 | 131 | if (null !== $components = $query->getData('components')) { 132 | $serializedComponents = is_string($components) ? $components : $this->serializeComponents($components); 133 | $url .= sprintf('&components=%s', urlencode($serializedComponents)); 134 | } 135 | 136 | return $this->fetchUrl($url, $query->getLocale(), $query->getLimit(), $query->getData('region', $this->region)); 137 | } 138 | 139 | public function reverseQuery(ReverseQuery $query): Collection 140 | { 141 | $coordinate = $query->getCoordinates(); 142 | $url = sprintf(self::REVERSE_ENDPOINT_URL_SSL, $coordinate->getLatitude(), $coordinate->getLongitude()); 143 | 144 | if (null !== $locationType = $query->getData('location_type')) { 145 | $url .= '&location_type='.urlencode($locationType); 146 | } 147 | 148 | if (null !== $resultType = $query->getData('result_type')) { 149 | $url .= '&result_type='.urlencode($resultType); 150 | } 151 | 152 | return $this->fetchUrl($url, $query->getLocale(), $query->getLimit(), $query->getData('region', $this->region)); 153 | } 154 | 155 | public function getName(): string 156 | { 157 | return 'google_maps'; 158 | } 159 | 160 | /** 161 | * @return string query with extra params 162 | */ 163 | private function buildQuery(string $url, ?string $locale = null, ?string $region = null): string 164 | { 165 | if (null === $this->apiKey && null === $this->clientId) { 166 | throw new InvalidCredentials('You must provide an API key. Keyless access was removed in June, 2016'); 167 | } 168 | 169 | if (null !== $locale) { 170 | $url = sprintf('%s&language=%s', $url, $locale); 171 | } 172 | 173 | if (null !== $region) { 174 | $url = sprintf('%s®ion=%s', $url, $region); 175 | } 176 | 177 | if (null !== $this->apiKey) { 178 | $url = sprintf('%s&key=%s', $url, $this->apiKey); 179 | } 180 | 181 | if (null !== $this->clientId) { 182 | $url = sprintf('%s&client=%s', $url, $this->clientId); 183 | 184 | if (null !== $this->channel) { 185 | $url = sprintf('%s&channel=%s', $url, $this->channel); 186 | } 187 | 188 | if (null !== $this->privateKey) { 189 | $url = $this->signQuery($url); 190 | } 191 | } 192 | 193 | return $url; 194 | } 195 | 196 | /** 197 | * @throws InvalidServerResponse 198 | * @throws InvalidCredentials 199 | */ 200 | private function fetchUrl(string $url, ?string $locale = null, int $limit = 1, ?string $region = null): AddressCollection 201 | { 202 | $url = $this->buildQuery($url, $locale, $region); 203 | $content = $this->getUrlContents($url); 204 | $json = $this->validateResponse($url, $content); 205 | 206 | // no result 207 | if (!isset($json->results) || !count($json->results) || 'OK' !== $json->status) { 208 | return new AddressCollection([]); 209 | } 210 | 211 | $results = []; 212 | foreach ($json->results as $result) { 213 | $builder = new AddressBuilder($this->getName()); 214 | $this->parseCoordinates($builder, $result); 215 | 216 | // set official Google place id 217 | if (isset($result->place_id)) { 218 | $builder->setValue('id', $result->place_id); 219 | } 220 | 221 | // update address components 222 | foreach ($result->address_components as $component) { 223 | foreach ($component->types as $type) { 224 | $this->updateAddressComponent($builder, $type, $component); 225 | } 226 | } 227 | 228 | /** @var GoogleAddress $address */ 229 | $address = $builder->build(GoogleAddress::class); 230 | $address = $address->withId($builder->getValue('id')); 231 | if (isset($result->geometry->location_type)) { 232 | $address = $address->withLocationType($result->geometry->location_type); 233 | } 234 | if (isset($result->types)) { 235 | $address = $address->withResultType($result->types); 236 | } 237 | if (isset($result->formatted_address)) { 238 | $address = $address->withFormattedAddress($result->formatted_address); 239 | } 240 | 241 | $results[] = $address 242 | ->withStreetAddress($builder->getValue('street_address')) 243 | ->withIntersection($builder->getValue('intersection')) 244 | ->withPolitical($builder->getValue('political')) 245 | ->withColloquialArea($builder->getValue('colloquial_area')) 246 | ->withWard($builder->getValue('ward')) 247 | ->withNeighborhood($builder->getValue('neighborhood')) 248 | ->withPremise($builder->getValue('premise')) 249 | ->withSubpremise($builder->getValue('subpremise')) 250 | ->withNaturalFeature($builder->getValue('natural_feature')) 251 | ->withAirport($builder->getValue('airport')) 252 | ->withPark($builder->getValue('park')) 253 | ->withPointOfInterest($builder->getValue('point_of_interest')) 254 | ->withEstablishment($builder->getValue('establishment')) 255 | ->withSubLocalityLevels($builder->getValue('subLocalityLevel', [])) 256 | ->withPostalCodeSuffix($builder->getValue('postal_code_suffix')) 257 | ->withPartialMatch($result->partial_match ?? false); 258 | 259 | if (count($results) >= $limit) { 260 | break; 261 | } 262 | } 263 | 264 | return new AddressCollection($results); 265 | } 266 | 267 | /** 268 | * Update current resultSet with given key/value. 269 | * 270 | * @param string $type Component type 271 | * @param object $values The component values 272 | */ 273 | private function updateAddressComponent(AddressBuilder $builder, string $type, $values): void 274 | { 275 | switch ($type) { 276 | case 'postal_code': 277 | $builder->setPostalCode($values->long_name); 278 | 279 | break; 280 | 281 | case 'locality': 282 | case 'postal_town': 283 | $builder->setLocality($values->long_name); 284 | 285 | break; 286 | 287 | case 'administrative_area_level_1': 288 | case 'administrative_area_level_2': 289 | case 'administrative_area_level_3': 290 | case 'administrative_area_level_4': 291 | case 'administrative_area_level_5': 292 | $builder->addAdminLevel(intval(substr($type, -1)), $values->long_name, $values->short_name); 293 | 294 | break; 295 | 296 | case 'sublocality_level_1': 297 | case 'sublocality_level_2': 298 | case 'sublocality_level_3': 299 | case 'sublocality_level_4': 300 | case 'sublocality_level_5': 301 | $subLocalityLevel = $builder->getValue('subLocalityLevel', []); 302 | $subLocalityLevel[] = [ 303 | 'level' => intval(substr($type, -1)), 304 | 'name' => $values->long_name, 305 | 'code' => $values->short_name, 306 | ]; 307 | $builder->setValue('subLocalityLevel', $subLocalityLevel); 308 | 309 | break; 310 | 311 | case 'country': 312 | $builder->setCountry($values->long_name); 313 | $builder->setCountryCode($values->short_name); 314 | 315 | break; 316 | 317 | case 'street_number': 318 | $builder->setStreetNumber($values->long_name); 319 | 320 | break; 321 | 322 | case 'route': 323 | $builder->setStreetName($values->long_name); 324 | 325 | break; 326 | 327 | case 'sublocality': 328 | $builder->setSubLocality($values->long_name); 329 | 330 | break; 331 | 332 | case 'street_address': 333 | case 'intersection': 334 | case 'political': 335 | case 'colloquial_area': 336 | case 'ward': 337 | case 'neighborhood': 338 | case 'premise': 339 | case 'subpremise': 340 | case 'natural_feature': 341 | case 'airport': 342 | case 'park': 343 | case 'point_of_interest': 344 | case 'establishment': 345 | case 'postal_code_suffix': 346 | $builder->setValue($type, $values->long_name); 347 | 348 | break; 349 | 350 | default: 351 | } 352 | } 353 | 354 | /** 355 | * Sign a URL with a given crypto key 356 | * Note that this URL must be properly URL-encoded 357 | * src: http://gmaps-samples.googlecode.com/svn/trunk/urlsigning/UrlSigner.php-source. 358 | * 359 | * @param string $query Query to be signed 360 | * 361 | * @return string $query query with signature appended 362 | */ 363 | private function signQuery(string $query): string 364 | { 365 | $url = parse_url($query); 366 | 367 | $urlPartToSign = $url['path'].'?'.$url['query']; 368 | 369 | // Decode the private key into its binary format 370 | $decodedKey = base64_decode(str_replace(['-', '_'], ['+', '/'], $this->privateKey)); 371 | 372 | // Create a signature using the private key and the URL-encoded 373 | // string using HMAC SHA1. This signature will be binary. 374 | $signature = hash_hmac('sha1', $urlPartToSign, $decodedKey, true); 375 | 376 | $encodedSignature = str_replace(['+', '/'], ['-', '_'], base64_encode($signature)); 377 | 378 | return sprintf('%s&signature=%s', $query, $encodedSignature); 379 | } 380 | 381 | /** 382 | * Serialize the component query parameter. 383 | * 384 | * @param array $components 385 | */ 386 | private function serializeComponents(array $components): string 387 | { 388 | return implode('|', array_map(function ($name, $value) { 389 | return sprintf('%s:%s', $name, $value); 390 | }, array_keys($components), $components)); 391 | } 392 | 393 | /** 394 | * Decode the response content and validate it to make sure it does not have any errors. 395 | * 396 | * @param string $content 397 | * 398 | * @return \Stdclass result form json_decode() 399 | * 400 | * @throws InvalidCredentials 401 | * @throws InvalidServerResponse 402 | * @throws QuotaExceeded 403 | */ 404 | private function validateResponse(string $url, $content) 405 | { 406 | // Throw exception if invalid clientID and/or privateKey used with GoogleMapsBusinessProvider 407 | if (false !== strpos($content, "Provided 'signature' is not valid for the provided client ID")) { 408 | throw new InvalidCredentials(sprintf('Invalid client ID / API Key %s', $url)); 409 | } 410 | 411 | $json = json_decode($content); 412 | 413 | // API error 414 | if (!isset($json)) { 415 | throw InvalidServerResponse::create($url); 416 | } 417 | 418 | if ('REQUEST_DENIED' === $json->status && 'The provided API key is invalid.' === $json->error_message) { 419 | throw new InvalidCredentials(sprintf('API key is invalid %s', $url)); 420 | } 421 | 422 | if ('REQUEST_DENIED' === $json->status) { 423 | throw new InvalidServerResponse(sprintf('API access denied. Request: %s - Message: %s', $url, $json->error_message)); 424 | } 425 | 426 | // you are over your quota 427 | if ('OVER_QUERY_LIMIT' === $json->status) { 428 | throw new QuotaExceeded(sprintf('Daily quota exceeded %s', $url)); 429 | } 430 | 431 | return $json; 432 | } 433 | 434 | /** 435 | * Parse coordinates and bounds. 436 | * 437 | * @param \Stdclass $result 438 | */ 439 | private function parseCoordinates(AddressBuilder $builder, $result): void 440 | { 441 | $coordinates = $result->geometry->location; 442 | $builder->setCoordinates($coordinates->lat, $coordinates->lng); 443 | 444 | if (isset($result->geometry->bounds)) { 445 | $builder->setBounds( 446 | $result->geometry->bounds->southwest->lat, 447 | $result->geometry->bounds->southwest->lng, 448 | $result->geometry->bounds->northeast->lat, 449 | $result->geometry->bounds->northeast->lng 450 | ); 451 | } elseif (isset($result->geometry->viewport)) { 452 | $builder->setBounds( 453 | $result->geometry->viewport->southwest->lat, 454 | $result->geometry->viewport->southwest->lng, 455 | $result->geometry->viewport->northeast->lat, 456 | $result->geometry->viewport->northeast->lng 457 | ); 458 | } elseif ('ROOFTOP' === $result->geometry->location_type) { 459 | // Fake bounds 460 | $builder->setBounds( 461 | $coordinates->lat, 462 | $coordinates->lng, 463 | $coordinates->lat, 464 | $coordinates->lng 465 | ); 466 | } 467 | } 468 | } 469 | --------------------------------------------------------------------------------