matanyadaev/laravel-eloquent-spatial
Laravel Eloquent Spatial adds spatial data types and query functions to Eloquent models. Easily work with geometry, points, polygons, distances, and spatial indexes across MySQL and PostgreSQL, with expressive casts, scopes, and query builders.
composer require matanyadaev/laravel-eloquent-spatial
Publish the config if you need to change the default SRID or column naming conventions:
php artisan vendor:publish --provider="Matanyadaev\LaravelSpatial\LaravelSpatialServiceProvider"
Define a spatial column and cast it to a geometry type:
// migration
Schema::create('places', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->point('location'); // or ->geometry(), ->polygon(), etc.
$table->spatialIndex('location');
});
use Matanyadaev\LaravelSpatial\Types\Point;
class Place extends Model
{
protected $casts = ['location' => Point::class];
}
First use case — "10 nearest places to me":
$me = new Point(51.5074, -0.1278); // confirm lat/lng vs x/y in the README
Place::query()
->orderByDistance('location', $me)
->limit(10)
->get();
Where to look first:
config/laravel-spatial.php — SRID defaults, column type mapping, GeoJSON/WKT serialization options.src/Types — the geometry value objects; this is what your casts return.src/Query (or the builder macros) — where whereDistance, whereWithin, whereContains, orderByDistance are registered.Cast the column once, query it everywhere. Don't wrap geometry in accessors that rebuild points on every read; let the cast do the work and keep the raw column as the single source of truth.
Compose with the normal builder. The spatial methods are macros, so they chain with scopes, where, pagination, and eager loads:
Place::query()
->where('is_active', true)
->whereDistance('location', $me, '<', 5_000) // check argument signature/units
->orderByDistance('location', $me)
->paginate(25);
Prefilter, then order. For "nearest N" over a large table, add a cheap bounding-box/radius filter first so the spatial index can be used, then orderByDistance on the smaller set. Ordering alone is usually a full scan.
Containment and overlap for regions. Store polygons on areas or zones and use whereContains('boundary', $point) to answer "which zone am I in?" rather than doing it in PHP.
GeoJSON at the edge. Eloquent casts are convenient but don't assume the JSON shape is what Leaflet/Mapbox wants. Normalize in an API resource:
'geometry' => $place->location->jsonSerialize(), // verify GeoJSON vs WKT in config
Factories and seeders work naturally once the cast is registered:
Place::factory()->create([
'location' => new Point(fake()->latitude(), fake()->longitude()),
]);
Pick one production driver and match it in CI. MySQL 8 and PostGIS are the well-trodden paths; SQLite is fine for unit tests of value objects but not for query behavior.
Consistent SRID. Use 4326 for WGS84 lat/lng everywhere unless you have a specific reason (web mercator 3857 for tiles). Mixing SRIDs is the #1 source of "why is this distance zero" bugs.
ST_Distance_Sphere and PostGIS geography return meters; raw ST_Distance on a 4326 geometry returns degrees. Don't copy a radius constant between environments without checking.:memory: database will throw on geometry columns unless spatialite is loaded. Keep spatial query tests on MySQL/PostGIS.toSql() shows ?, and getBindings() may contain binary blobs — dump with bin2hex() rather than dd(), or you'll get unreadable output.whereWithin/whereContains-style predicates; they generally do not accelerate orderByDistance.null, not an empty point — guard in resources and Blade templates.toArray()/toJson() emits WKT, GeoJSON, or a plain string in your configured version; frontend map libraries expect GeoJSON.DB::raw / selectRaw for functions the package doesn't expose, and extend the base geometry class if you need a domain-specific type (e.g., a validated Point with application invariants).How can I help you explore Laravel packages today?