Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Google Map Laravel Package

ivory/google-map

Laravel package for adding Google Maps to your application with a simple, configurable API. Build maps, add markers and info windows, and customize map options in Blade views without writing JavaScript by hand.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require bresam/ivory-google-map

This is a fork of egeloen/ivory-google-map. The namespace is still Ivory\GoogleMap\*, which is what all existing docs/StackOverflow answers use — search under the old package name when you hit references you don't recognise.

Where to look first

  1. Ivory\GoogleMap\Map — the central DTO. Everything else attaches to it.
  2. Ivory\GoogleMap\Helper\MapHelper — turns a Map into HTML/JS/CSS.
  3. Ivory\GoogleMap\Base\Coordinate — the (latitude, longitude) value object you will construct constantly.
  4. Ivory\GoogleMap\Overlays\Marker and InfoWindow — the 90% case.
  5. Ivory\GoogleMap\Services\* — server-side geocoding, directions, distance matrix, elevation, timezone, places.

Minimal first use case (map + marker + popup)

use Ivory\GoogleMap\Map;
use Ivory\GoogleMap\Base\Coordinate;
use Ivory\GoogleMap\Overlays\Marker;
use Ivory\GoogleMap\Overlays\InfoWindow;
use Ivory\GoogleMap\Helper\MapHelper;

$map = new Map();
$map->setApiKey(config('services.google.maps_key'));
$map->setHtmlId('office-map');
$map->setCenter(new Coordinate(48.8566, 2.3522));
$map->setZoom(12);
$map->setWidth('100%');
$map->setHeight('400px');

$marker = new Marker(new Coordinate(48.8566, 2.3522));
$infoWindow = new InfoWindow();
$infoWindow->setContent('<strong>HQ</strong>');
$marker->setInfoWindow($infoWindow);
$map->addMarker($marker);

$html = (new MapHelper())->render($map);

In a Blade view: {!! $html !!}. That's it — one <div>, one <script> tag, one stylesheet block. No assets to publish, no migrations, no config.

For the very first iteration, prefer the static map path if you only need a picture (no interactivity): it avoids pulling the JS API into the page entirely and is cheaper against your quota.

Implementation Patterns

Controller → View handoff. Build the Map in a controller (or a form-request-validated action) and hand the rendered HTML string to Blade. Do not build maps inside Blade — the Map/Marker API is imperative and gets ugly inline. If you must, wrap it in a dedicated App\Support\MapBuilder class.

A Laravel service provider is required. The package ships zero Laravel integration — no auto-discovery, no facade, no config. Register it yourself:

// App\Providers\GoogleMapServiceProvider
$this->app->bind(MapHelper::class, fn () => new MapHelper());
$this->app->singleton(Map::class, function () {
    $map = new Map();
    $map->setApiKey(config('services.google.maps_key'));
    return $map;
});

Resolve from the container as app(Map::class) — but never as a singleton per-request if you render multiple maps on one page (see Gotchas). Bind (not singleton) Map, singleton the MapHelper.

Granular rendering for CSP / deferred scripts. MapHelper::render() emits stylesheet + div + inline JS together. Use the split methods when you need control:

$helper = new MapHelper();
$helper->renderStylesheet($map); // <style> — head
$helper->renderHtml($map);       // <div>    — body
$helper->renderJavascript($map); // <script> — bottom

This is also how you debug: renderJavascript() shows you exactly what the helper generates given your Map object.

Geocoding is a server-side service, not a map feature. Use Ivory\GoogleMap\Services\Geocoding\Geocoder with the Google provider to turn a street address into a Coordinate, then feed that to your Marker. Treat it as a cacheable lookup: wrap in Cache::remember("geocode:{$address}", now()->addDays(30), ...) — Google's billing is per-request and addresses don't move.

use Ivory\GoogleMap\Services\Geocoding\Geocoder;
use Ivory\GoogleMap\Services\Geocoding\Provider\GoogleMaps;

$geocoder = new Geocoder(new GoogleMaps());
$geocoder->setApiKey(config('services.google.maps_key'));
$response = $geocoder->geocode('10 Downing Street, London');
$coordinate = $response->getResults()[0]->getGeometry()->getLocation();

Batch/route services are the same shape. Services\Directions\Directions, Services\DistanceMatrix\DistanceMatrix, Services\Elevation\ElevationService all follow the "construct with key, call with ->route(...)/->process(...), read a response object" pattern. In practice these belong in a queue job, not a web request — they're slow and billable.

Marker + interaction pattern. Custom click handlers are not a first-class concept; the idiom is to set a JS callback name on the marker and define the function in a @push('scripts') block:

$marker->setOptions(['clickable' => true]);

then hand-write the listener in Blade with google.maps.event.addListener(map_office-map, 'click', ...). Keep the Map a pure data description; put behaviour in view-layer JS.

Multiple maps on one page. Create a fresh Map per instance, each with a distinct setHtmlId('...'). The helper has no ID derivation — collisions silently produce a blank second map.

Gotchas and Tips

  • An API key is mandatory, not optional. Google's 2016 policy change broke every pre-2016 tutorial that omits it. setApiKey() must be set, and the key must have Maps JavaScript API enabled in the GCP console — a key scoped only to Geocoding will render an empty grey box with a "for development purposes only" watermark.
  • Coordinate is (latitude, longitude). Yes, the same order as every map service except the ones that take [lng, lat]. You will swap these once; assert them in a test.
  • Do not register Map as a singleton. A shared instance accumulates markers across the request lifecycle — in Octane it leaks across requests. Bind fresh.
  • Multiple maps need distinct HTML IDs — setHtmlId() on each, or one overwrites the other's container.
  • setAutoZoom() and the center/zoom pair fight each other. If setAutoZoom(true) is on (the default), your setZoom() value is ignored and the map fits its overlays. Turn it off explicitly if you want a fixed zoom.
  • InfoWindow content is inserted as raw HTML. Passing user-supplied text straight into setContent() is an XSS vector — the library does not escape it for you. e() in Blade, then pass the escaped string.
  • The map div has no content inside — don't put fallback text in the Blades. Use setHtml()/setOptions(['class' => ...]) if you need to style the container; styling the surrounding wrapper is safer.
  • Rendering order matters with Turbo/HTMX/livewire re-renders. The inline <script> runs on evaluate; a partial swap re-executes it against a DOM node that may already be initialised. Either force a full reload of the map container or call the generated init function after swap.
  • Async loading. setAsync(true) defers map init until the JS API is ready; combine with setLibraries(['places', 'geometry']) if you use those services. Forgetting a library surfaces as undefined is not a function in the console, not a PHP error.
  • Service responses differ in shape per service. There's no common Response interface — Geocoding returns results[].geometry.location, Directions returns routes[].legs[].steps[]. Don't try to normalise across them preemptively.
  • Exceptions are HTTP-level, not semantic. Services throw on non-2xx (bad key, over-quota). A ZERO_RESULTS geocode is not an exception — check $response->getResults() for an empty array or you'll get a null-pointer on [0].
  • Extension points: Map::setOptions(), Marker::setOptions(), InfoWindow::setOptions() are plain pass-throughs into the generated JS. This is the escape hatch for anything the package doesn't model (custom icons, animation, optimized flags) — you rarely need to fork.
  • Testing is pleasant. Map, Marker, Coordinate are pure value objects. Assert on MapHelper::renderJavascript($map) for output shape, and mock the Services\* classes via a thin interface you own so you never hit Google in tests.
  • Adoption risk. 13 stars, 0 known dependents, but the 2025-09-04 release means it is maintained. The API is stable and the code is small enough to vendor-and-patch if the fork stops. Pin the version in composer.json and don't rely on it staying on Packagist forever — the original egeloen repo is the historical proof of concept for that failure mode.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons