grimzy/laravel-mysql-spatial
Laravel package for working with MySQL spatial data types and functions. Adds Eloquent support for geometry columns like POINT, LINESTRING and POLYGON, with spatial queries and helpers for storing, retrieving and querying geographic data in your models.
This package adds MySQL spatial column types, typed geometry objects, and Eloquent query helpers to Laravel. It is a good fit for store locators, delivery zones, geofencing, and any feature where you would otherwise hand-write ST_* SQL.
Requirements and install
composer require grimzy/laravel-mysql-spatial
Pin the version explicitly if you are upgrading an existing app, since 5.0.0 is a major release:
composer require grimzy/laravel-mysql-spatial:^5.0
Where to look first
README.md for the supported type and query examples.CHANGELOG.md / release notes for 5.0.0 — it is a major version bump, so check for renamed helpers or changed signatures before upgrading.src/Types/ for Point, LineString, Polygon, MultiPoint, etc.src/Eloquent/SpatialTrait.php for how attributes are cast and persisted.Minimal first use case: a radius search
Migration:
Schema::create('places', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->point('location', 4326); // SRID 4326 for GPS coordinates
$table->spatialIndex('location');
$table->timestamps();
});
Model:
use Grimzy\LaravelMysqlSpatial\Eloquent\SpatialTrait;
class Place extends Model
{
use SpatialTrait;
protected $fillable = ['name', 'location'];
protected $spatialFields = ['location'];
}
Write and read:
$place = Place::create([
'name' => 'HQ',
'location' => new Point(37.7749, -122.4194, 4326),
]);
$place->location->getLat();
$place->location->getLng();
Query nearby places:
$center = new Point(37.7749, -122.4194, 4326);
$nearby = Place::whereDistanceSphere('location', $center, '<=', 5000)
->orderByDistanceSphere('location', $center)
->get();
If the query macro names differ in your installed version, check the README/tests and use whereRaw('ST_Distance_Sphere(location, POINT(?, ?)) <= ?', [...]) as a fallback.
Wrap spatial filters in model scopes
Keep the query language out of controllers and make the units explicit:
public function scopeNearby(Builder $query, Point $center, int $meters): Builder
{
return $query
->whereDistanceSphere('location', $center, '<=', $meters)
->orderByDistanceSphere('location', $center);
}
Then compose normally:
Place::where('active', true)
->nearby($center, 5000)
->paginate(20);
Geofencing with polygons
Use Polygon for delivery zones or service areas. For point-in-polygon checks, the stored point should be within the polygon:
$zone = Polygon::fromWKT(
'POLYGON((...))',
4326
);
$inside = Place::whereWithin('location', $zone)->get();
whereIntersects is useful for overlapping regions; whereContains is the inverse direction and argument order matters.
Ingesting GeoJSON and WKT from APIs
The geometry types can be constructed from common formats:
$point = Point::fromWKT('POINT(37.7749 -122.4194)', 4326);
$geometry = Geometry::fromJson($request->input('geojson'), 4326);
Normalize incoming data at the boundary, then store typed geometry objects on the model.
Return GeoJSON from APIs
Geometry objects can serialize to GeoJSON. A common pattern is to build a Feature manually so you control property names:
return [
'type' => 'Feature',
'geometry' => json_decode($place->location->toJson(), true),
'properties' => [
'name' => $place->name,
],
];
If you expect toArray() to emit GeoJSON automatically, verify the package version; otherwise add an accessor or $appends entry.
Use raw SQL as the escape hatch
The package covers the common predicates, but PostGIS-style operations such as buffers, envelopes, and GeoJSON output often need raw expressions:
Place::whereRaw(
'ST_Contains(ST_GeomFromText(?, 4326), location)',
[$wkt]
)->get();
Place::selectRaw('*, ST_AsGeoJSON(location) as location_geojson')->get();
Do not mix raw selected GeoJSON aliases with the model's spatial cast unless you alias them away from the original column name.
Bulk operations
The trait only applies to Eloquent model assignments. Bulk inserts through DB::table() bypass casting, so use raw geometry constructors:
DB::table('places')->insert([
'name' => 'Warehouse',
'location' => DB::raw("ST_GeomFromText('POINT(37.7749 -122.4194)', 4326)"),
]);
For a small number of rows, creating models is safer because validation and casting run.
Testing
Use a MySQL connection in phpunit.xml; SQLite will fail on point, spatialIndex, and ST_* functions. Factories should assign geometry objects:
'location' => new Point(37.7749, -122.4194, 4326),
Maintenance and framework compatibility
5.0.0 is the current line. Its stated target is Laravel 8 with MySQL 5.7, which means:
composer.json constraints before adopting it on a newer app; you will likely hit a dependency conflict at install time rather than a runtime error, which is at least easy to detect.ST_Distance/ST_Contains semantics, test on 8.x.matanyadaev/laravel-eloquent-spatial or clickbar/laravel-magellan, or vendor a fork.Spatial indexes require NOT NULL
In MySQL, columns in a SPATIAL INDEX generally must be NOT NULL. A common failure is:
$table->point('location', 4326)->nullable();
$table->spatialIndex('location'); // may fail
Either make the column non-nullable with a sensible default, or omit the spatial index and accept slower predicate queries.
SRID and axis order
Point uses latitude/longitude order, while GeoJSON coordinates are longitude/latitude. MySQL 8 with SRID 4326 also follows the SRID's axis order. Mixing SRID 0 and SRID 4326 values can produce errors such as "binary geometry function given two geometries of different SRIDs." Be explicit:
new Point($lat, $lng, 4326);
and pass the same SRID when constructing query geometries.
Distance units are not interchangeable
distance / planar ST_Distance returns units of the coordinate system; with SRID 0 this is effectively degrees, not meters.distanceSphere / ST_Distance_Sphere returns meters and should be used for radius searches with GPS coordinates.Do not compare a meter value against a planar distance query.
ST_Distance does not use the spatial index
Radius queries that order by distance will scan. A practical pattern is a bounding-box prefilter with whereWithin or whereIntersects using an envelope polygon, followed by orderByDistanceSphere for exact ordering. For high-traffic endpoints, consider a generated column or a dedicated geohash/grid column.
Casting is driven by $spatialFields
If you forget the trait or misspell a column in $spatialFields, Eloquent returns raw WKB/binary rather than a Point. If you use selectRaw, column aliases, or partial selects, the cast can break; include the original spatial column or handle the alias separately.
Assign geometry objects, not strings
Assignment expects a geometry instance:
$place->location = new Point(37.7749, -122.4194, 4326);
If you have WKT from an external source, parse it first with Point::fromWKT() or Geometry::fromWKT(). Do not assume a string will be cast automatically.
Polygon validity
Polygon rings must be closed (first point equals last point) and contain at least four points. LineString needs at least two. fromWKT will usually validate this; manually built arrays may produce MySQL errors.
Do not rely on exact coordinate equality
Coordinates are floating-point values. Use ST_Equals, a small distance tolerance, or compare rounded values rather than = on geometry columns.
Changing spatial columns is risky
$table->point(...)->change() is not reliably supported for spatial types. Prefer dropping and re-adding the column in a migration, and test on a copy of production data.
Version-specific query macro names
The package has exposed spatial query helpers under names such as whereDistanceSphere, orderByDistanceSphere, whereWithin, and related predicates, but naming has varied across releases — and a major version bump is exactly when this is most likely to bite. Confirm against your installed version's README or tests. When in doubt, whereRaw with bound WKT and SRID is predictable and easy to debug with toSql() and getBindings().
How can I help you explore Laravel packages today?