NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

The package works with zero extra configuration once the key is set — every value has a default and most are env-backed. The published config/google-places.php:

return [
    'key' => env('GOOGLE_PLACES_API_KEY'),

    'hosts' => [
        'places' => env('GOOGLE_PLACES_HOST', 'https://places.googleapis.com/v1'),
        'routes' => env('GOOGLE_ROUTES_HOST', 'https://routes.googleapis.com'),
        'geocoding' => env('GOOGLE_GEOCODING_HOST', 'https://maps.googleapis.com/maps/api'),
    ],

    'field_masks' => [
        'details' => 'id,displayName,formattedAddress,location,viewport,types,regularOpeningHours,photos',
        'autocomplete' => 'suggestions.placePrediction.placeId,suggestions.placePrediction.text,suggestions.placePrediction.structuredFormat,suggestions.placePrediction.types',
        'search' => 'places.id,places.displayName,places.formattedAddress,places.location,places.viewport,places.types,places.regularOpeningHours,places.photos',
        'routes' => 'originIndex,destinationIndex,distanceMeters,duration,condition,status',
    ],

    'http' => [
        'timeout' => env('GOOGLE_PLACES_TIMEOUT', 10),
        'connect_timeout' => env('GOOGLE_PLACES_CONNECT_TIMEOUT', 5),
        'retries' => env('GOOGLE_PLACES_RETRIES', 2),
        'retry_delay' => env('GOOGLE_PLACES_RETRY_DELAY', 200),
    ],

    'cache' => [
        'enabled' => env('GOOGLE_PLACES_CACHE', false),
        'store' => env('GOOGLE_PLACES_CACHE_STORE'),
        'ttl' => env('GOOGLE_PLACES_CACHE_TTL', 86400),
    ],

    'pagination' => [
        'max_pages' => env('GOOGLE_PLACES_MAX_PAGES', 5),
    ],

    'logging' => [
        'enabled' => env('GOOGLE_PLACES_LOGGING', false),
        'channel' => env('GOOGLE_PLACES_LOG_CHANNEL'),
    ],

    'rate_limits' => [
        'owner' => env('GOOGLE_PLACES_RATELIMIT_OWNER', 'app'),

        'places' => [
            'enabled' => env('GOOGLE_PLACES_PLACES_RATELIMIT_ENABLED', true),
            'limit' => env('GOOGLE_PLACES_PLACES_RATELIMIT', 600),
            'per' => env('GOOGLE_PLACES_PLACES_RATELIMIT_PER', 'minute'),
            'adaptive' => env('GOOGLE_PLACES_PLACES_RATELIMIT_ADAPTIVE', true),
            'max_wait' => env('GOOGLE_PLACES_PLACES_RATELIMIT_MAX_WAIT'),
            'jitter' => env('GOOGLE_PLACES_PLACES_RATELIMIT_JITTER'),
        ],

        // 'routes' => [...] and 'geocoding' => [...] — same keys, own env prefix
    ],
];

Every key

Every value is read strictly: a key that is not set takes its default, and anything invalid throws RoundlyConsulting\PackageToolkit\Exceptions\InvalidConfigurationException naming the key — never a silent fallback. Blank means not set: an absent key, null and a blank value (a host’s KEY=, empty or whitespace only) all take the default.

  • A bool switch accepts true/false, 1/0, on/off or yes/no, from .env or the published file.
  • An int takes an integer or an integer string ('30'). 'five', '5.5', '5s' or a value out of range throws. Timeouts, cache.ttl, pagination.max_pages and the rate-limit limit must be at least 1; retries, retry_delay, max_wait and jitter at least 0.
  • rate_limits.{surface}.per must be exactly second, minute, hour or day.
  • A string — hosts.*, field_masks.*, cache.store, logging.channel, rate_limits.owner — must be a string. A blank host is Google’s own, and a blank store, channel or owner the default. A field mask has no default: a blank one throws “required but missing”.
KeyDefaultEnvPurpose
keynullGOOGLE_PLACES_API_KEYAPI key. Sent as X-Goog-Api-Key (Places/Routes) and key= (Geocoding). Required.
hosts.placeshttps://places.googleapis.com/v1GOOGLE_PLACES_HOSTPlaces API (New) host. A string; blank = not set → Google’s own host.
hosts.routeshttps://routes.googleapis.comGOOGLE_ROUTES_HOSTRoutes API host (distance/ETA). A string; blank = not set → Google’s own host.
hosts.geocodinghttps://maps.googleapis.com/maps/apiGOOGLE_GEOCODING_HOSTGeocoding API host (reverse and forward geocoding). A string; blank = not set → Google’s own host.
field_masks.detailssee config—X-Goog-FieldMask for place details. Every mask must be a string; masks have no fallback, so a blank one throws (required but missing).
field_masks.autocompletesee config—X-Goog-FieldMask for autocomplete.
field_masks.searchsee config—X-Goog-FieldMask for text/nearby search.
field_masks.routessee config—X-Goog-FieldMask for distance and the route matrix.
http.timeout10GOOGLE_PLACES_TIMEOUTRequest timeout (seconds), at least 1.
http.connect_timeout5GOOGLE_PLACES_CONNECT_TIMEOUTConnection timeout (seconds), at least 1.
http.retries2GOOGLE_PLACES_RETRIESRetries on connection failure, 0 or more.
http.retry_delay200GOOGLE_PLACES_RETRY_DELAYDelay between retries (ms), 0 or more.
cache.enabledfalseGOOGLE_PLACES_CACHECache idempotent lookups (details, geocoding, distance, matrix, search).
cache.storenullGOOGLE_PLACES_CACHE_STORECache store; unset, null or blank = the default store. A non-string value throws.
cache.ttl86400GOOGLE_PLACES_CACHE_TTLCache TTL (seconds), at least 1.
pagination.max_pages5GOOGLE_PLACES_MAX_PAGESSafety cap for the paginated search helpers, at least 1; a warning is logged when hit.
logging.enabledfalseGOOGLE_PLACES_LOGGINGLog every request (endpoint, status, duration). The API key is never logged.
logging.channelnullGOOGLE_PLACES_LOG_CHANNELLog channel; unset, null or blank = the default channel. A non-string value throws.
rate_limits.ownerappGOOGLE_PLACES_RATELIMIT_OWNERBucket owner shared across surfaces (google-places:{surface}:{owner}). A string; blank = not set → app.
rate_limits.{surface}.enabledtrueGOOGLE_PLACES_{SURFACE}_RATELIMIT_ENABLEDThrottle this surface (places / routes / geocoding); false = unthrottled.
rate_limits.{surface}.limit600GOOGLE_PLACES_{SURFACE}_RATELIMITMax requests per window, at least 1.
rate_limits.{surface}.perminuteGOOGLE_PLACES_{SURFACE}_RATELIMIT_PERWindow: exactly second, minute, hour or day; anything else throws.
rate_limits.{surface}.adaptivetrueGOOGLE_PLACES_{SURFACE}_RATELIMIT_ADAPTIVESelf-tune from a 429 Retry-After.
rate_limits.{surface}.max_waitnullGOOGLE_PLACES_{SURFACE}_RATELIMIT_MAX_WAITFail fast (ms, 0 or more) instead of pacing; null or blank = pace.
rate_limits.{surface}.jitternullGOOGLE_PLACES_{SURFACE}_RATELIMIT_JITTERRandom spread (ms, 0 or more) added to a deferred call; null or blank = none.

Field masks

The Places API (New) and the Routes API require an X-Goog-FieldMask header naming the fields to return — a request without one errors. The defaults are conservative; trim them so you pay only for the fields you use, or extend them when you need more. For example, typed address components on a place need addressComponents in the details mask:

// config/google-places.php — add addressComponents so Place::components() is populated
'field_masks' => [
    'details' => 'id,displayName,formattedAddress,location,viewport,types,regularOpeningHours,photos,addressComponents',
    // ...
],

A single details request can also override its mask without touching config — see Place details.

Hosts

Each Google product lives on its own host. Override the hosts only when you proxy Google through your own gateway. Reverse and forward geocoding intentionally use the Geocoding API, which authenticates with the key query parameter; Places and Routes use the X-Goog-Api-Key header.

Environment

The common knobs are env-driven, so you rarely need to publish the config at all:

GOOGLE_PLACES_API_KEY=your-google-maps-api-key

GOOGLE_PLACES_TIMEOUT=10
GOOGLE_PLACES_CONNECT_TIMEOUT=5
GOOGLE_PLACES_RETRIES=2
GOOGLE_PLACES_RETRY_DELAY=200

GOOGLE_PLACES_CACHE=true
GOOGLE_PLACES_CACHE_TTL=86400

GOOGLE_PLACES_LOGGING=true
GOOGLE_PLACES_LOG_CHANNEL=stack

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.