petrepatrasc/google-map-bundle
A Symfony bundle for integrating Google Maps into your application, helping you configure map instances, markers, and frontend assets from PHP. It simplifies embedding interactive maps and managing map-related settings with minimal setup in Symfony projects.
First, accept the framing: you are not installing this package. petrepatrasc/google-map-bundle is a Symfony 2 bundle (AppKernel registration, a DependencyInjection/*Extension, XML/YAML service definitions, Twig helpers). Laravel has no bundle concept, so composer require petrepatrasc/google-map-bundle gets you a package that boots nothing, resolves no services, and registers no Blade directives. At 0 stars, 0 dependents, and a Symfony 2.x baseline, there is no version of this you should take a runtime dependency on.
What you do with it day-to-day is read it and reimplement the thin slice it wraps. The reusable payload is the underlying, framework-neutral map-builder library it depends on (an egeloen/ivory-google-map-style core). The bundle layer itself — DI wiring, config normalization, Twig rendering — is exactly the part you will throw away and rewrite in a few hundred lines.
Minimal setup order:
composer show -a petrepatrasc/google-map-bundle (or read composer.json on GitHub) and identify the wrapped library + its version. Then inspect whether the builder/encoder classes carry Symfony annotations or container lookups. If they do, stop — treat it as a rewrite, not an extraction. If they're clean, you have a candidate for a vendored helper.app/config/config.yml API-key pattern translates to config/services.php + .env:// config/services.php
'google_maps' => [
'browser_key' => env('GOOGLE_MAPS_BROWSER_KEY'),
'server_key' => env('GOOGLE_MAPS_SERVER_KEY'),
'signing_secret' => env('GOOGLE_MAPS_SIGNING_SECRET'), // only if premium plan
],
GOOGLE_MAPS_BROWSER_KEY=AIza... # HTTP-referrer restricted
GOOGLE_MAPS_SERVER_KEY=AIza... # IP-restricted, never sent to the browser
{{-- resources/views/components/map.blade.php --}}
@props(['id' => 'map', 'center' => ['lat' => 0, 'lng' => 0], 'zoom' => 12, 'markers' => []])
<div id="{{ $id }}" style="height:400px" data-center='@json($center)' data-zoom="{{ $zoom }}"
data-markers='@json($markers)'></div>
@once
<script>
window.initMaps = window.initMaps || [];
</script>
<script async
src="https://maps.googleapis.com/maps/api/js?key={{ config('services.google_maps.browser_key') }}&loading=async&callback=__gmapsReady">
</script>
@push('scripts')
<script>
window.__gmapsReady = () => window.initMaps.forEach(fn => fn());
</script>
@endpush
@endonce
Do not port the bundle's inline <script> emission verbatim. Older Symfony-era helpers emit inline JS that a nonce-based CSP will block; see Gotchas.
1. Provider + config translation, not bundle translation. If you do extract the core classes, the mapping table is short and worth internalizing before you start estimating:
| Bundle artifact | Laravel equivalent | Mechanical port? |
|---|---|---|
*Bundle class |
ServiceProvider (package discovery) |
No — rewrite |
DependencyInjection/Configuration tree |
config/*.php + mergeConfigFrom() |
Schema only |
| XML/YAML service defs | Container bindings | No — rewrite |
| Twig extension helpers | Blade component / directive | No — rewrite |
| Map builder / encoder classes | Framework-agnostic service | Yes, if truly decoupled |
Only the last row is reusable. Validate it before spending time on the rest.
2. Treat map config as a DTO, not an array soup. The bundle's value was normalizing options; keep that idea but make it typed and testable:
final class MapView
{
public function __construct(
public readonly array $center, // ['lat' => 51.5, 'lng' => -0.12]
public readonly int $zoom = 12,
public readonly array $markers = [],
) {}
public static function fromModels(iterable $stores): self
{
return new self(
center: ['lat' => (float) config('services.maps.default_lat'),
'lng' => (float) config('services.maps.default_lng')],
markers: collect($stores)->map(fn ($s) => [
'lat' => (float) $s->latitude,
'lng' => (float) $s->longitude,
'title' => $s->name,
])->all(),
);
}
}
Pass it to the component as ['lat' => ..., 'lng' => ...] and @json it into the data attribute. This is where the bundle gave you "clean presentation logic" — a view-model plus a Blade component gives you the same thing without the dependency.
3. Keep server-side work server-side. Two things genuinely belong in PHP rather than the browser: static map URLs and polyline encoding. Both are pure functions and both are exactly what you'd extract from the bundle's core — or reimplement in ~150 lines.
// app/Support/Maps/StaticMap.php
public function url(array $markers, int $width = 600, int $height = 400): string
{
$path = '/maps/api/staticmap';
$params = [
'size' => "{$width}x{$height}",
'maptype' => 'roadmap',
'markers' => collect($markers)->map(fn ($m) => "{$m['lat']},{$m['lng']}")->implode('|'),
'key' => config('services.google_maps.server_key'),
];
ksort($params);
$query = http_build_query($params);
if ($secret = config('services.google_maps.signing_secret')) {
$sig = rtrim(strtr(base64_encode(
hash_hmac('sha1', $path.'?'.$query, $secret, true)
), '+/', '-_'), '=');
$query .= '&signature='.$sig;
}
return 'https://maps.googleapis.com'.$path.'?'.$query;
}
4. Cache geocoding results with a hard TTL. Google's ToS caps how long you may retain geocoded coordinates. Don't let a Cache::rememberForever() creep in:
$coords = Cache::remember("geocode:{$hash}", now()->addDays(29), function () use ($address) {
return Http::get('https://maps.googleapis.com/maps/api/geocode/json', [
'address' => $address,
'key' => config('services.google_maps.server_key'),
])->throw()->json('results.0.geometry.location');
});
5. Lazy-initialize maps behind a callback registry. The modern JS API wants loading=async&callback=. Push one initializer per map onto window.initMaps and let a single callback drain it. This mirrors the bundle's "render container, then render init script" split but is SPA/Turbo-safe:
document.addEventListener('turbo:load', () => {
document.querySelectorAll('[data-map]').forEach(el => {
const map = new google.maps.Map(el, {
center: JSON.parse(el.dataset.center),
zoom: +el.dataset.zoom,
});
JSON.parse(el.dataset.markers).forEach(m =>
new google.maps.Marker({ position: m, map, title: m.title }));
el._mapInstance = map; // hold reference; see Gotchas
});
});
6. Test the encoders, mock the network. Http::fake() for geocode/directions; a fixture-based test for polyline encoding against a known encoded string; and a snapshot assertion on the static-map URL shape (including signature) so an API drift shows up as a failing test, not a broken page.
7. Cut over behind a feature flag. Map rendering is stateless. Roll out per-route with Feature::active('maps') in the component, keep the old surface for one release, flip back if Google returns errors.
composer.json pinned around PHP 5.x and code that uses curly-brace string offsets, each(), or dynamic properties — all removed or deprecated by PHP 8. Laravel 10/11 requires 8.1+, so any extraction needs an explicit modernization pass and a version pin in your own composer.json.<script> from Twig helpers will break a nonce CSP. If you port rendering logic, emit a Blade component with a @push('scripts') block and nonce it, or you'll spend an afternoon chasing blocked scripts.v=weekly (or omit) plus loading=async&callback=, never a pinned v=3.x string.config/services.php must.BillingNotEnabledMapError. Budget for this before feature work — it's a procurement task, not a code task.RefererNotAllowedMapError = referrer restriction mismatch (note localhost needs its own entry). ApiNotActivatedMapError = Maps JS API not enabled on the project. InvalidKeyMapError = wrong key for the surface. Check the browser network tab first; the server side is rarely the culprit for blank maps..env on dev machines, that's fine — but treat production as a secrets-manager value.@json in a Blade attribute is XSS-safe only if you keep the quoting. Use single quotes around the attribute, or Js::from() on Laravel 9+, which escapes correctly for embedded JSON.el._mapInstance) and call google.maps.event.clearInstanceListeners(el._mapInstance) on teardown. The bundle's "render once per page" assumption doesn't survive client-side navigation.Markers tank the frame rate. Use @googlemaps/markerclusterer — don't expect the wrapper to have solved this.DECIMAL, Eloquent casts, or form input will silently become strings. Cast to float in the DTO or google.maps.LatLng will warn and occasionally misplace the pin.How can I help you explore Laravel packages today?