vendor/geocoder-php/google-maps-provider/GoogleMaps.php line 104

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4.  * This file is part of the Geocoder package.
  5.  * For the full copyright and license information, please view the LICENSE
  6.  * file that was distributed with this source code.
  7.  *
  8.  * @license    MIT License
  9.  */
  10. namespace Geocoder\Provider\GoogleMaps;
  11. use Geocoder\Collection;
  12. use Geocoder\Exception\InvalidCredentials;
  13. use Geocoder\Exception\InvalidServerResponse;
  14. use Geocoder\Exception\QuotaExceeded;
  15. use Geocoder\Exception\UnsupportedOperation;
  16. use Geocoder\Http\Provider\AbstractHttpProvider;
  17. use Geocoder\Model\AddressBuilder;
  18. use Geocoder\Model\AddressCollection;
  19. use Geocoder\Provider\GoogleMaps\Model\GoogleAddress;
  20. use Geocoder\Provider\Provider;
  21. use Geocoder\Query\GeocodeQuery;
  22. use Geocoder\Query\ReverseQuery;
  23. use Psr\Http\Client\ClientInterface;
  24. /**
  25.  * @author William Durand <william.durand1@gmail.com>
  26.  */
  27. final class GoogleMaps extends AbstractHttpProvider implements Provider
  28. {
  29.     /**
  30.      * @var string
  31.      */
  32.     const GEOCODE_ENDPOINT_URL_SSL 'https://maps.googleapis.com/maps/api/geocode/json?address=%s';
  33.     /**
  34.      * @var string
  35.      */
  36.     const REVERSE_ENDPOINT_URL_SSL 'https://maps.googleapis.com/maps/api/geocode/json?latlng=%F,%F';
  37.     /**
  38.      * @var string|null
  39.      */
  40.     private $region;
  41.     /**
  42.      * @var string|null
  43.      */
  44.     private $apiKey;
  45.     /**
  46.      * @var string|null
  47.      */
  48.     private $clientId;
  49.     /**
  50.      * @var string|null
  51.      */
  52.     private $privateKey;
  53.     /**
  54.      * @var string|null
  55.      */
  56.     private $channel;
  57.     /**
  58.      * Google Maps for Business
  59.      * https://developers.google.com/maps/documentation/business/
  60.      * Maps for Business is no longer accepting new signups.
  61.      *
  62.      * @param ClientInterface $client     An HTTP adapter
  63.      * @param string          $clientId   Your Client ID
  64.      * @param string          $privateKey Your Private Key (optional)
  65.      * @param string          $region     Region biasing (optional)
  66.      * @param string          $apiKey     Google Geocoding API key (optional)
  67.      * @param string          $channel    Google Channel parameter (optional)
  68.      *
  69.      * @return GoogleMaps
  70.      */
  71.     public static function business(
  72.         ClientInterface $client,
  73.         string $clientId,
  74.         string $privateKey null,
  75.         string $region null,
  76.         string $apiKey null,
  77.         string $channel null
  78.     ) {
  79.         $provider = new self($client$region$apiKey);
  80.         $provider->clientId $clientId;
  81.         $provider->privateKey $privateKey;
  82.         $provider->channel $channel;
  83.         return $provider;
  84.     }
  85.     /**
  86.      * @param ClientInterface $client An HTTP adapter
  87.      * @param string          $region Region biasing (optional)
  88.      * @param string          $apiKey Google Geocoding API key (optional)
  89.      */
  90.     public function __construct(ClientInterface $clientstring $region nullstring $apiKey null)
  91.     {
  92.         parent::__construct($client);
  93.         $this->region $region;
  94.         $this->apiKey $apiKey;
  95.     }
  96.     public function geocodeQuery(GeocodeQuery $query): Collection
  97.     {
  98.         // Google API returns invalid data if IP address given
  99.         // This API doesn't handle IPs
  100.         if (filter_var($query->getText(), FILTER_VALIDATE_IP)) {
  101.             throw new UnsupportedOperation('The GoogleMaps provider does not support IP addresses, only street addresses.');
  102.         }
  103.         $url sprintf(self::GEOCODE_ENDPOINT_URL_SSLrawurlencode($query->getText()));
  104.         if (null !== $bounds $query->getBounds()) {
  105.             $url .= sprintf(
  106.                 '&bounds=%s,%s|%s,%s',
  107.                 $bounds->getSouth(),
  108.                 $bounds->getWest(),
  109.                 $bounds->getNorth(),
  110.                 $bounds->getEast()
  111.             );
  112.         }
  113.         if (null !== $components $query->getData('components')) {
  114.             $serializedComponents is_string($components) ? $components $this->serializeComponents($components);
  115.             $url .= sprintf('&components=%s'urlencode($serializedComponents));
  116.         }
  117.         return $this->fetchUrl($url$query->getLocale(), $query->getLimit(), $query->getData('region'$this->region));
  118.     }
  119.     public function reverseQuery(ReverseQuery $query): Collection
  120.     {
  121.         $coordinate $query->getCoordinates();
  122.         $url sprintf(self::REVERSE_ENDPOINT_URL_SSL$coordinate->getLatitude(), $coordinate->getLongitude());
  123.         if (null !== $locationType $query->getData('location_type')) {
  124.             $url .= '&location_type='.urlencode($locationType);
  125.         }
  126.         if (null !== $resultType $query->getData('result_type')) {
  127.             $url .= '&result_type='.urlencode($resultType);
  128.         }
  129.         return $this->fetchUrl($url$query->getLocale(), $query->getLimit(), $query->getData('region'$this->region));
  130.     }
  131.     /**
  132.      * {@inheritdoc}
  133.      */
  134.     public function getName(): string
  135.     {
  136.         return 'google_maps';
  137.     }
  138.     /**
  139.      * @param string $url
  140.      * @param string $locale
  141.      *
  142.      * @return string query with extra params
  143.      */
  144.     private function buildQuery(string $urlstring $locale nullstring $region null): string
  145.     {
  146.         if (null === $this->apiKey && null === $this->clientId) {
  147.             throw new InvalidCredentials('You must provide an API key. Keyless access was removed in June, 2016');
  148.         }
  149.         if (null !== $locale) {
  150.             $url sprintf('%s&language=%s'$url$locale);
  151.         }
  152.         if (null !== $region) {
  153.             $url sprintf('%s&region=%s'$url$region);
  154.         }
  155.         if (null !== $this->apiKey) {
  156.             $url sprintf('%s&key=%s'$url$this->apiKey);
  157.         }
  158.         if (null !== $this->clientId) {
  159.             $url sprintf('%s&client=%s'$url$this->clientId);
  160.             if (null !== $this->channel) {
  161.                 $url sprintf('%s&channel=%s'$url$this->channel);
  162.             }
  163.             if (null !== $this->privateKey) {
  164.                 $url $this->signQuery($url);
  165.             }
  166.         }
  167.         return $url;
  168.     }
  169.     /**
  170.      * @param string $url
  171.      * @param string $locale
  172.      * @param int    $limit
  173.      * @param string $region
  174.      *
  175.      * @return AddressCollection
  176.      *
  177.      * @throws InvalidServerResponse
  178.      * @throws InvalidCredentials
  179.      */
  180.     private function fetchUrl(string $urlstring $locale nullint $limitstring $region null): AddressCollection
  181.     {
  182.         $url $this->buildQuery($url$locale$region);
  183.         $content $this->getUrlContents($url);
  184.         $json $this->validateResponse($url$content);
  185.         // no result
  186.         if (!isset($json->results) || !count($json->results) || 'OK' !== $json->status) {
  187.             return new AddressCollection([]);
  188.         }
  189.         $results = [];
  190.         foreach ($json->results as $result) {
  191.             $builder = new AddressBuilder($this->getName());
  192.             $this->parseCoordinates($builder$result);
  193.             // set official Google place id
  194.             if (isset($result->place_id)) {
  195.                 $builder->setValue('id'$result->place_id);
  196.             }
  197.             // update address components
  198.             foreach ($result->address_components as $component) {
  199.                 foreach ($component->types as $type) {
  200.                     $this->updateAddressComponent($builder$type$component);
  201.                 }
  202.             }
  203.             /** @var GoogleAddress $address */
  204.             $address $builder->build(GoogleAddress::class);
  205.             $address $address->withId($builder->getValue('id'));
  206.             if (isset($result->geometry->location_type)) {
  207.                 $address $address->withLocationType($result->geometry->location_type);
  208.             }
  209.             if (isset($result->types)) {
  210.                 $address $address->withResultType($result->types);
  211.             }
  212.             if (isset($result->formatted_address)) {
  213.                 $address $address->withFormattedAddress($result->formatted_address);
  214.             }
  215.             $results[] = $address
  216.                 ->withStreetAddress($builder->getValue('street_address'))
  217.                 ->withIntersection($builder->getValue('intersection'))
  218.                 ->withPolitical($builder->getValue('political'))
  219.                 ->withColloquialArea($builder->getValue('colloquial_area'))
  220.                 ->withWard($builder->getValue('ward'))
  221.                 ->withNeighborhood($builder->getValue('neighborhood'))
  222.                 ->withPremise($builder->getValue('premise'))
  223.                 ->withSubpremise($builder->getValue('subpremise'))
  224.                 ->withNaturalFeature($builder->getValue('natural_feature'))
  225.                 ->withAirport($builder->getValue('airport'))
  226.                 ->withPark($builder->getValue('park'))
  227.                 ->withPointOfInterest($builder->getValue('point_of_interest'))
  228.                 ->withEstablishment($builder->getValue('establishment'))
  229.                 ->withSubLocalityLevels($builder->getValue('subLocalityLevel', []))
  230.                 ->withPostalCodeSuffix($builder->getValue('postal_code_suffix'))
  231.                 ->withPartialMatch($result->partial_match ?? false);
  232.             if (count($results) >= $limit) {
  233.                 break;
  234.             }
  235.         }
  236.         return new AddressCollection($results);
  237.     }
  238.     /**
  239.      * Update current resultSet with given key/value.
  240.      *
  241.      * @param AddressBuilder $builder
  242.      * @param string         $type    Component type
  243.      * @param object         $values  The component values
  244.      */
  245.     private function updateAddressComponent(AddressBuilder $builderstring $type$values)
  246.     {
  247.         switch ($type) {
  248.             case 'postal_code':
  249.                 $builder->setPostalCode($values->long_name);
  250.                 break;
  251.             case 'locality':
  252.             case 'postal_town':
  253.                 $builder->setLocality($values->long_name);
  254.                 break;
  255.             case 'administrative_area_level_1':
  256.             case 'administrative_area_level_2':
  257.             case 'administrative_area_level_3':
  258.             case 'administrative_area_level_4':
  259.             case 'administrative_area_level_5':
  260.                 $builder->addAdminLevel(intval(substr($type, -1)), $values->long_name$values->short_name);
  261.                 break;
  262.             case 'sublocality_level_1':
  263.             case 'sublocality_level_2':
  264.             case 'sublocality_level_3':
  265.             case 'sublocality_level_4':
  266.             case 'sublocality_level_5':
  267.                 $subLocalityLevel $builder->getValue('subLocalityLevel', []);
  268.                 $subLocalityLevel[] = [
  269.                     'level' => intval(substr($type, -1)),
  270.                     'name' => $values->long_name,
  271.                     'code' => $values->short_name,
  272.                 ];
  273.                 $builder->setValue('subLocalityLevel'$subLocalityLevel);
  274.                 break;
  275.             case 'country':
  276.                 $builder->setCountry($values->long_name);
  277.                 $builder->setCountryCode($values->short_name);
  278.                 break;
  279.             case 'street_number':
  280.                 $builder->setStreetNumber($values->long_name);
  281.                 break;
  282.             case 'route':
  283.                 $builder->setStreetName($values->long_name);
  284.                 break;
  285.             case 'sublocality':
  286.                 $builder->setSubLocality($values->long_name);
  287.                 break;
  288.             case 'street_address':
  289.             case 'intersection':
  290.             case 'political':
  291.             case 'colloquial_area':
  292.             case 'ward':
  293.             case 'neighborhood':
  294.             case 'premise':
  295.             case 'subpremise':
  296.             case 'natural_feature':
  297.             case 'airport':
  298.             case 'park':
  299.             case 'point_of_interest':
  300.             case 'establishment':
  301.             case 'postal_code_suffix':
  302.                 $builder->setValue($type$values->long_name);
  303.                 break;
  304.             default:
  305.         }
  306.     }
  307.     /**
  308.      * Sign a URL with a given crypto key
  309.      * Note that this URL must be properly URL-encoded
  310.      * src: http://gmaps-samples.googlecode.com/svn/trunk/urlsigning/UrlSigner.php-source.
  311.      *
  312.      * @param string $query Query to be signed
  313.      *
  314.      * @return string $query query with signature appended
  315.      */
  316.     private function signQuery(string $query): string
  317.     {
  318.         $url parse_url($query);
  319.         $urlPartToSign $url['path'].'?'.$url['query'];
  320.         // Decode the private key into its binary format
  321.         $decodedKey base64_decode(str_replace(['-''_'], ['+''/'], $this->privateKey));
  322.         // Create a signature using the private key and the URL-encoded
  323.         // string using HMAC SHA1. This signature will be binary.
  324.         $signature hash_hmac('sha1'$urlPartToSign$decodedKeytrue);
  325.         $encodedSignature str_replace(['+''/'], ['-''_'], base64_encode($signature));
  326.         return sprintf('%s&signature=%s'$query$encodedSignature);
  327.     }
  328.     /**
  329.      * Serialize the component query parameter.
  330.      *
  331.      * @param array $components
  332.      *
  333.      * @return string
  334.      */
  335.     private function serializeComponents(array $components): string
  336.     {
  337.         return implode('|'array_map(function ($name$value) {
  338.             return sprintf('%s:%s'$name$value);
  339.         }, array_keys($components), $components));
  340.     }
  341.     /**
  342.      * Decode the response content and validate it to make sure it does not have any errors.
  343.      *
  344.      * @param string $url
  345.      * @param string $content
  346.      *
  347.      * @return \Stdclass result form json_decode()
  348.      *
  349.      * @throws InvalidCredentials
  350.      * @throws InvalidServerResponse
  351.      * @throws QuotaExceeded
  352.      */
  353.     private function validateResponse(string $url$content)
  354.     {
  355.         // Throw exception if invalid clientID and/or privateKey used with GoogleMapsBusinessProvider
  356.         if (false !== strpos($content"Provided 'signature' is not valid for the provided client ID")) {
  357.             throw new InvalidCredentials(sprintf('Invalid client ID / API Key %s'$url));
  358.         }
  359.         $json json_decode($content);
  360.         // API error
  361.         if (!isset($json)) {
  362.             throw InvalidServerResponse::create($url);
  363.         }
  364.         if ('REQUEST_DENIED' === $json->status && 'The provided API key is invalid.' === $json->error_message) {
  365.             throw new InvalidCredentials(sprintf('API key is invalid %s'$url));
  366.         }
  367.         if ('REQUEST_DENIED' === $json->status) {
  368.             throw new InvalidServerResponse(sprintf('API access denied. Request: %s - Message: %s'$url$json->error_message));
  369.         }
  370.         // you are over your quota
  371.         if ('OVER_QUERY_LIMIT' === $json->status) {
  372.             throw new QuotaExceeded(sprintf('Daily quota exceeded %s'$url));
  373.         }
  374.         return $json;
  375.     }
  376.     /**
  377.      * Parse coordinates and bounds.
  378.      *
  379.      * @param AddressBuilder $builder
  380.      * @param \Stdclass      $result
  381.      */
  382.     private function parseCoordinates(AddressBuilder $builder$result)
  383.     {
  384.         $coordinates $result->geometry->location;
  385.         $builder->setCoordinates($coordinates->lat$coordinates->lng);
  386.         if (isset($result->geometry->bounds)) {
  387.             $builder->setBounds(
  388.                 $result->geometry->bounds->southwest->lat,
  389.                 $result->geometry->bounds->southwest->lng,
  390.                 $result->geometry->bounds->northeast->lat,
  391.                 $result->geometry->bounds->northeast->lng
  392.             );
  393.         } elseif (isset($result->geometry->viewport)) {
  394.             $builder->setBounds(
  395.                 $result->geometry->viewport->southwest->lat,
  396.                 $result->geometry->viewport->southwest->lng,
  397.                 $result->geometry->viewport->northeast->lat,
  398.                 $result->geometry->viewport->northeast->lng
  399.             );
  400.         } elseif ('ROOFTOP' === $result->geometry->location_type) {
  401.             // Fake bounds
  402.             $builder->setBounds(
  403.                 $coordinates->lat,
  404.                 $coordinates->lng,
  405.                 $coordinates->lat,
  406.                 $coordinates->lng
  407.             );
  408.         }
  409.     }
  410. }