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.
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
Ivory\GoogleMap\Map — the central DTO. Everything else attaches to it.Ivory\GoogleMap\Helper\MapHelper — turns a Map into HTML/JS/CSS.Ivory\GoogleMap\Base\Coordinate — the (latitude, longitude) value object you will construct constantly.Ivory\GoogleMap\Overlays\Marker and InfoWindow — the 90% case.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.
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.
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.Map as a singleton. A shared instance accumulates markers across the request lifecycle — in Octane it leaks across requests. Bind fresh.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.setContent() is an XSS vector — the library does not escape it for you. e() in Blade, then pass the escaped string.setHtml()/setOptions(['class' => ...]) if you need to style the container; styling the surrounding wrapper is safer.<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.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.Response interface — Geocoding returns results[].geometry.location, Directions returns routes[].legs[].steps[]. Don't try to normalise across them preemptively.ZERO_RESULTS geocode is not an exception — check $response->getResults() for an empty array or you'll get a null-pointer on [0].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.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.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.How can I help you explore Laravel packages today?