ghanem/google-map
A Laravel package that makes it easy to embed Google Maps in your app. It provides Blade components and a fluent PHP API for rendering maps, markers, info windows, and shapes, with configurable options for building interactive, location-aware interfaces without heavy JavaScript boilerplate.
Install and wire it up
composer require ghanem/google-map
php artisan vendor:publish --provider="Ghanem\GoogleMap\FieldServiceProvider" # config + assets, tag names in the README
Requires Laravel Nova 4 or 5 (nova >= 4.0); check composer.json of the release you pull — Nova 5 support landed late, so pin if you're on an older Nova.
Get a browser key, not a server key. In Google Cloud Console enable Maps JavaScript API and Places API (the autocomplete widget will silently do nothing if only Maps JS is enabled). Create an API key, restrict it by HTTP referrer to your Nova host(s), and put it in .env:
GOOGLE_MAPS_API_KEY=AIza...
The package reads the key from the published config (config/google-map.php) or config('services.google.maps_api_key') depending on version — open the published config file first and follow whatever key it actually reads. Then php artisan config:cache.
Migration and first field. The field maps to columns you define; the common shape is three columns:
Schema::table('stores', function (Blueprint $table) {
$table->decimal('latitude', 10, 7)->nullable();
$table->decimal('longitude', 10, 7)->nullable();
$table->string('address')->nullable();
});
use Ghanem\GoogleMap\GoogleMap;
public function fields(NovaRequest $request): array
{
return [
GoogleMap::make('Location')
->latitude('latitude')
->longitude('longitude')
->address('address')
->required(),
];
}
Start with one field on one Resource form — a "store address" or "venue location" — and confirm both the marker and the address input round-trip on save. That single use case is 90% of what this field is for.
Separate columns vs. a single JSON column. Two viable shapes:
$table->json('location') plus protected $casts = ['location' => 'asArrayObject'], and configure the field to hydrate the whole attribute. Less schema churn, but every read needs a cast.If the field hydrates separate attributes, be aware it fills them from the field's payload on save; if you need derived values (city, country, formatted address from Places address_components), do it in fillUsing() or a model saving observer rather than trusting the browser payload.
Displaying it outside the form. Don't leave the field on index — the map component is heavy and useless in a table cell. Use:
GoogleMap::make('Location')->onlyOnForms(),
Text::make('Address')->onlyOnIndex()->sortable(),
Text::make('Coordinates', fn ($m) => "{$m->latitude}, {$m->longitude}")->onlyOnDetail(),
Or link out: ->displayUsing(fn ($v, $m) => ...) returning a https://www.google.com/maps/search/?api=1&query=lat,lng anchor (wrap in new HtmlString via a custom Text subclass, since Nova escapes by default).
Distance queries. Once lat/lng live in columns you get the real payoff: a Haversine scope for "stores near me", then feed the result into an index table or a Distance filter. Keep the math in SQL (ST_Distance_Sphere on MySQL 8 / PostGIS ST_DWithin), not in PHP.
Geocoding on the server. For imports, seeders and API-created records there is no browser: geocode in a queued job using googlemaps/google-maps-services-php or a plain HTTP call, write lat/lng, and let the Nova field open in read-only style for those records. Never geocode synchronously in a saving hook — a flaky Google call becomes a failed form submit and a billable request per keystroke of retry.
Programmatic prefills and defaults. For "copy billing address to store address" style flows, set defaults via ->default() where supported, or push values through ->withMeta() from the Resource so the component centres correctly. If the map keeps resetting on dependsOn() re-evaluation, that's the reactive re-render, not your data.
Read-only / audit views. On detail pages, prefer the field with a locked/disabled variant or skip it entirely and render a static image via the Static Maps API — cheaper and no JS on a page users only glance at.
places library (or vice versa). Enable both during bring-up, then trim.config:cache traps. An env var added after a cached config will not appear; re-run php artisan config:cache on every deploy. If the key is passed through the field's metadata rather than config(), a VIEW_COMPILED cache won't help you — clear Nova's asset cache with php artisan nova:publish after upgrades.decimal(10,7)/decimal(11,8) and cast to float on read. Strings compare badly and JSON floats round-trip unevenly; (0, 0) means "no marker", which drops you in the Gulf of Guinea — always set a default map centre.Flexible/panel layouts. Autocomplete dropdowns render inside a stacking context; inside Modal or Nova 5 panels the suggestions list can clip or sit behind the footer. Either keep the field top-level on the form or bump the container's overflow/z-index in a published Vue override.->rules() and again in a FormRequest; the marker position is just JSON in the request.styles array through the field options.assertSee to prove the map rendered.component() to point at your own published Vue component when you need custom behaviour (radius overlay, marker dragging, "use my location" button). The published assets and config are the intended seams; there are no other public APIs to lean on.composer.json and keep the published Vue component in your repo so a Nova 6 breaking change doesn't strand you. Budget half a day to fork-and-maintain if that day ever comes.How can I help you explore Laravel packages today?