ivory/google-map-bundle
Ivory Google Map Bundle integrates Google Maps into Symfony applications, providing services and Twig helpers to render maps, markers, info windows, and controls, with configurable map options and geocoding support.
Install and enable
composer require ivory/google-map-bundle
// config/bundles.php
Ivory\GoogleMapBundle\IvoryGoogleMapBundle::class => ['all' => true],
# config/packages/ivory_google_map.yaml
ivory_google_map:
api_key: '%env(GOOGLE_MAPS_BROWSER_KEY)%'
language: 'en'
region: 'US'
Check vendor/ivory/google-map-bundle/composer.json first — this is a low-traffic fork (9 stars, 0 dependents) of the abandoned egeloen/ivory-google-map-bundle, so confirm the Symfony constraint matches your app before you build on it. Pin the exact versions.
Understand the two layers
ivory/google-map — the real library: a pure-PHP object model (Map, Coordinate, Marker, InfoWindow, overlay managers, JS builders).ivory/google-map-bundle — thin Symfony glue: DI services (ivory_google_map.map, ivory_google_map.helper.*, the geocoder/directions/elevation HTTP services) plus Twig functions ivory_google_map() and ivory_google_map_js().Almost everything you'll debug lives in the first layer. Read Ivory\GoogleMap\Map, Overlay\Marker, Overlay\InfoWindow, and Helper\MapHelper before anything else.
First use case: server-rendered display map
// src/Controller/StoreController.php
use Ivory\GoogleMap\Base\Coordinate;
use Ivory\GoogleMap\Map;
use Ivory\GoogleMap\Overlay\Marker;
use Ivory\GoogleMap\Overlay\InfoWindow;
$map = new Map();
$map->setHtmlId('store-map'); // stable id, do this explicitly
$map->setCenter(new Coordinate(48.8566, 2.3522)); // lat, lon — in that order
$map->setZoom(13);
foreach ($stores as $store) {
$marker = new Marker(new Coordinate($store->getLat(), $store->getLng()));
$marker->setInfoWindow(new InfoWindow('<strong>'.htmlspecialchars($store->getName()).'</strong>'));
$map->getOverlayManager()->addMarker($marker); // older versions: $map->addMarker()
}
return $this->render('store/index.html.twig', ['map' => $map]);
{# templates/store/index.html.twig #}
<div class="map-wrapper">{{ ivory_google_map(map) }}</div>
{% block javascripts %}
{{ ivory_google_map_js(map) }}
{% endblock %}
Two calls, two jobs: ivory_google_map() emits the container <div>, ivory_google_map_js() emits the <script> that loads the Maps JS API and instantiates the map and overlays. Render the container first, the script after.
Controller → Twig, with a map factory service. Don't scatter map construction across controllers. Extract a MapFactory that takes coordinates + options and returns a configured Map; controllers stay thin, and you can unit-test the factory (the Map object is pure PHP, no browser needed).
final class StoreMapFactory
{
public function create(iterable $stores): Map
{
$map = new Map();
$map->setHtmlId('store-map');
$map->setCenter(new Coordinate(48.8566, 2.3522));
$map->setZoom(12);
$map->getOverlayManager()->addMarkerClusterer($this->clusterer($stores));
return $map;
}
}
Many points → marker clustering, not more markers. For anything past a few dozen markers, build a MarkerClusterer and add markers to it rather than to the map. Emitting thousands of individual new google.maps.Marker() calls in inline JS is the usual cause of "page feels slow" reports.
Interactive behavior via the event manager. Server-side map building is fine for read-only display. When you need click handlers, drag callbacks, or form population, attach events with JsCode on the map's event manager and keep the callback bodies tiny — one function call into a small Stimulus controller that owns the real logic:
$map->getEventManager()->addEvent($marker, 'click', new JsCode('window.dispatchEvent(new CustomEvent("store:selected", {detail: '.$store->getId().'}))'));
Render the JS without tags when you need control. ivory_google_map_js(map, false) (second arg toggles the <script> wrapper) lets you place the payload in a deferred script or hand it to your asset pipeline, which is what you want with Turbo/Turbolinks or a CSP that forbids inline scripts.
Asset loading order with modern front-ends. Under Turbo Drive or htmx partial swaps, the container <div> is replaced but the map instance is not re-created. Re-run the generated JS on turbo:load / after the swap, or mark the wrapper data-turbo-permanent.
Server-side Google APIs are separate services. The bundle also wires geocoder / directions / distance-matrix / elevation clients over an HTTP adapter. Use these from a service with a cache layer (PSR-6) — geocoding the same address on every request is both slow and billable. These calls need a server key (IP-restricted), not the browser key.
Testing. Assert on the model, not the HTML: $map->getOverlayManager()->getMarkers() gives you a countable array; $map->getCenter()->getLatitude() etc. For the rendered output, one smoke assertion that the response contains your html id and maps.googleapis.com is enough.
ivory_google_map.map is a shared service. Injecting it and mutating it leaks state between maps in the same request and, in long-running workers (RoadRunner, FrankenPHP, Messenger), across requests. Always new Map().setHtmlId() explicitly for every map.ivory_google_map_js() in <head>, you get InvalidValueError: not an instance of Element or a silently blank canvas. Same failure mode if the div is inside a tab/modal that isn't in the DOM yet.RefererNotAllowedMapError. Browser key = HTTP-referrer restricted; server key = IP restricted. Two keys, two config entries.(latitude, longitude) everywhere, while Bound takes south-west/north-east corners. Swapped coordinates put your map in the ocean; there's no validation.JsCode. The builder does not escape your callback bodies. Use json_encode() for any dynamic values, never bare interpolation.ivory_google_map_js() — it's plain, readable google.maps.* calls. Copy it into the browser console to isolate whether a bug is in your PHP model or the JS.google.maps.Marker. The Maps JS API has moved to AdvancedMarkerElement; this bundle's JS builders still emit the classic marker. It works today but will eventually warn/break — one more reason to keep the map layer behind a small factory you can swap.ivory_google_map.helper.* service. Custom overlays: extend the library's base classes and register a matching JS builder. Custom map styling: pass a stylesheet array through the map options rather than post-processing the output.symfony/ux-style approach) is a contained change rather than a rewrite.How can I help you explore Laravel packages today?