yieldstudio/nova-google-polygon
A Laravel Nova field for drawing and editing polygons on Google Maps, letting admins capture map shapes and save their coordinates directly in your Nova resources.
Install and publish config
composer require yieldstudio/nova-google-polygon
php artisan vendor:publish --provider="YieldStudio\NovaGooglePolygon\FieldServiceProvider"
The publish tag/provider name may differ slightly from the snippet above — the package's README is thin, so php artisan vendor:publish and picking the nova-google-polygon entry is the fastest way to confirm. You'll get a config file holding the Google Maps API key used to render the map component.
Wire up the key
// config/nova-google-polygon.php
return [
'key' => env('GOOGLE_MAPS_API_KEY'),
];
You need Maps JavaScript API enabled in Google Cloud Console. The key is injected into the Nova frontend bundle, so it is public by definition — restrict it by HTTP referrer to your Nova hostname, and never reuse the same key for server-side Geocoding/Directions calls.
Add the column and the field
// migration
$table->json('service_area')->nullable();
use YieldStudio\NovaGooglePolygon\NovaGooglePolygon;
public function fields(NovaRequest $request)
{
return [
NovaGooglePolygon::make('Service Area', 'service_area')
->rules('nullable', 'json'),
];
}
Where to look first
src/ — the field class and its Vue component. The component is where the drawing behaviour lives; anything about defaults, output format, or map options will be in withMeta there.resources/js/ — check what event is emitted on draw/edit. That tells you the exact payload shape before you build the rest of your app around it.config/ — the only real configuration surface.First use case
The natural fit is a "delivery zone" or "service area" field: an admin draws a polygon over a map, the polygon is persisted as GeoJSON, and your pricing/dispatch logic asks "is this lat/lng inside the zone?" That's the whole loop — draw, store, query.
Storing GeoJSON, not a coordinate soup
The field will hand back a GeoJSON Feature/Polygon structure. Store it verbatim in a json column so you can later swap rendering libraries (Leaflet, Mapbox, Google Maps) without a data migration.
The model cast is the trap most people hit. If Nova submits a JSON string and you also cast the column to array, Laravel will json_encode the string and you get double-encoded garbage:
"\"{\\\"type\\\":\\\"Polygon\\\"...}\""
Two safe options:
// Option A: no cast, store raw string, decode at the point of use
protected $casts = []; // leave service_area alone
// Option B: a small normalising cast
class GeoJson implements CastsAttributes
{
public function get($model, $key, $value, $attributes)
{
return is_string($value) ? json_decode($value, true) : $value;
}
public function set($model, $key, $value, $attributes)
{
return [is_string($value) ? $value : json_encode($value)];
}
}
Verify which one you need by dumping $request->input('service_area') on the first save — don't assume.
Point-in-polygon checks server-side
Once stored, the useful work happens in PHP. Decode the polygon and test against a point:
use Phayes\GeoPHP\GeoPHP; // or a hand-rolled ray-casting function
$polygon = GeoPHP::load($zone->service_area, 'json');
$point = GeoPHP::load("POINT({$lng} {$lat})", 'wkt');
$isInZone = $polygon->contains($point);
Keep this in a dedicated ServiceArea value object / helper rather than inlining it in controllers — you'll call it from order validation, driver assignment, and pricing, and you'll want one place to fix coordinate-order bugs.
Multiple polygons per resource
If a zone needs several disjoint areas, wrap them in a MultiPolygon or a FeatureCollection and store the whole thing. The field itself is single-geometry in most versions; either extend the Vue component or add a repeater of sorts and merge on save in fillUsing.
Transform on the way in/out with Nova hooks
If the map component emits something you don't want in the DB (e.g. {lat, lng} objects rather than GeoJSON), normalise without forking the package:
NovaGooglePolygon::make('Area')
->fillUsing(function ($request, $model, $attribute) {
$raw = $request->input($attribute);
$model->{$attribute} = $this->toGeoJson($raw);
})
->resolveUsing(fn ($value) => $value);
Testing
Don't test the map — test the polygon. Put two or three real GeoJSON fixtures in tests/Fixtures/, seed them onto models, and assert your containment logic and your Nova save path. The Vue component itself isn't worth covering; the geometry and persistence are.
Consuming it on the frontend
For a public-facing map, send the stored GeoJSON straight into Google Maps' google.maps.Data layer or into Leaflet/Turf.js. Because you stored standard GeoJSON, this is a one-liner rather than a translation layer.
The package is archived and last released mid-2023. That's the headline. There will be no fixes for Nova 4/5 component API changes, no Vue 3 upgrades, no security patches. Before committing: install it, run php artisan nova:assets / npm run build, and actually load a form. If the component doesn't mount, you are maintaining a fork from day one — budget for it or pick another approach.
Coordinate order will bite you. GeoJSON is [longitude, latitude]. Google Maps' JS API is {lat, lng}. Every "my polygon is in the ocean off the coast of Africa" bug is this. Decide once, write it down, and add an assertion in your helper.
Polygon rings must close. In GeoJSON the first and last coordinate of a linear ring are identical. Google's DrawingManager often does not close the ring on the way out. If PostGIS or ST_GeomFromGeoJSON starts rejecting your data, that's why — append the first point to the end before persisting.
Restrict the API key by referrer, not by IP. Nova's assets are served to the browser, so IP restrictions will break and the key is visible in devtools to anyone with a Nova login. "This page didn't load Google Maps correctly" in the console is almost always a referrer or billing problem, not a code problem.
Empty vs. null. Deleting the polygon typically emits null, but some versions emit {"type":"Polygon","coordinates":[[]]} or []. Handle all three where you read the value, and add ->nullable() to both the column and the rules. A json validation rule alone won't catch a valid-but-empty polygon.
JSON column on MySQL vs. Postgres. MySQL json columns reformat and reorder keys on write, which is fine for reading but will break naive string comparisons in tests. Postgres jsonb is the better choice if you're on it and plan to query into the geometry later — though for real spatial queries, add a separate geometry column and backfill with PostGIS rather than querying the JSON.
Detail and index views are weak spots. The field is primarily a form control. Expect to customise resolveUsing/displayUsing or write a small custom IndexField if you want to show the polygon on the resource index — the component won't do that for you.
Extension point is the Vue component. Anything cosmetic — map height, default center, zoom, drawing controls, satellite vs. roadmap — lives in the published/registered JS, not in PHP. Publish the assets (vendor:publish --tag=nova-google-polygon-assets or via Nova::script) and edit the component rather than trying to fight it with meta flags.
Have an exit strategy. If the archived status is a dealbreaker, the same job is ~150 lines of code: a custom Nova field wrapping Google Maps DrawingManager or Leaflet.draw, emitting JSON.stringify(data.toGeoJSON()) into a hidden input. That's genuinely less work than owning a stale dependency, and it's the recommendation I'd give for anything with a multi-year lifespan.
How can I help you explore Laravel packages today?