Looking to hire Laravel developers? Try LaraJobs

laravel-spatial maintained by mohamedhabibwork

Description
Multi-database spatial data types extension for Laravel supporting MySQL 8+ and PostgreSQL 16+ (with PostGIS).
Last update
2025/12/06 23:15 (dev-main)
License
Links
Downloads
1

Comments
comments powered by Disqus

Laravel Spatial Extension

Build Status Code Climate Code Climate Packagist Packagist StyleCI license

Modern Laravel package for working with spatial data types and functions. Supports both MySQL 8+ and PostgreSQL 16+ (with PostGIS) using a transparent, unified API.

Features

  • 🌐 Multi-Database Support: Works seamlessly with MySQL 8+ and PostgreSQL 16+ (PostGIS)
  • 🔄 Transparent API: Same code works for both databases
  • 🚀 Modern PHP: Built with PHP 8.2+ features (typed properties, match expressions, readonly, etc.)
  • 📦 Laravel 11+: Full integration with Laravel's query builder and Eloquent ORM
  • 🎯 Type Safety: Comprehensive type hints and strict types throughout
  • 🗺️ SRID Support: Full support for Spatial Reference System Identifiers
  • 🔍 Spatial Functions: Distance calculations, geometric comparisons, and more

Database Support

MySQL 8+

PostgreSQL 16+ (PostGIS)

  • Requires PostGIS extension
  • Uses geometry(Point, 4326) type syntax
  • Spatial indexes with GIST INDEX
  • Full PostGIS function support

Requirements

  • PHP 8.2 or higher
  • Laravel 11.0 or higher
  • MySQL 8.0+ OR PostgreSQL 16+ with PostGIS extension

Installation

Install via Composer:

composer require mohamedhabibwork/laravel-spatial

PostgreSQL Setup

For PostgreSQL, you must enable the PostGIS extension:

CREATE EXTENSION IF NOT EXISTS postgis;

The package service provider is auto-discovered by Laravel. For manual registration, add to config/app.php:

'providers' => [
    Habib\LaravelSpatial\SpatialServiceProvider::class,
],

Version History

  • 6.x.x: MySQL 8+ and PostgreSQL 16+ support with PHP 8.2+ (Current)
  • 5.x.x: MySQL 5.7 and 8.0 (Laravel 8+)
  • 4.x.x: MySQL 8.0 with SRID support (Laravel 8+)
  • 3.x.x: MySQL 8.0 with SRID support (Laravel < 8.0)
  • 2.x.x: MySQL 5.7 and 8.0 (Laravel < 8.0)
  • 1.x.x: MySQL 5.6 and 5.5

Quickstart

Create a Migration

php artisan make:migration create_places_table

Define your spatial columns:

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('places', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            // Add spatial data fields
            $table->point('location')->nullable();
            $table->polygon('area')->nullable();
            $table->timestamps();
        });
        
        // With SRID (WGS84 spheroid)
        // Schema::create('places', function (Blueprint $table) {
        //     $table->id();
        //     $table->string('name')->unique();
        //     $table->point('location', 4326)->nullable();
        //     $table->polygon('area', 4326)->nullable();
        //     $table->timestamps();
        // });
    }

    public function down(): void
    {
        Schema::dropIfExists('places');
    }
};

Run the migration:

php artisan migrate

Create a Model

php artisan make:model Place

Use the SpatialTrait and define spatial fields:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Habib\LaravelSpatial\Eloquent\SpatialTrait;
use Habib\LaravelSpatial\Types\Point;
use Habib\LaravelSpatial\Types\Polygon;

class Place extends Model
{
    use SpatialTrait;

    protected $fillable = ['name'];

    protected $spatialFields = [
        'location',
        'area',
    ];
}

Save a Model

use App\Models\Place;
use Habib\LaravelSpatial\Types\Point;
use Habib\LaravelSpatial\Types\Polygon;
use Habib\LaravelSpatial\Types\LineString;

// Create a place with a point
$place = new Place();
$place->name = 'Empire State Building';
$place->location = new Point(40.7484404, -73.9878441);  // lat, lng
$place->save();

// With SRID
$place->location = new Point(40.7484404, -73.9878441, 4326);
$place->save();

// Create a polygon
$place->area = new Polygon([new LineString([
    new Point(40.74894149554006, -73.98615270853043),
    new Point(40.74848633046773, -73.98648262023926),
    new Point(40.747925497790725, -73.9851602911949),
    new Point(40.74837050671544, -73.98482501506805),
    new Point(40.74894149554006, -73.98615270853043)
])], 4326);
$place->save();

Retrieve a Model

$place = Place::first();
$lat = $place->location->getLat();  // 40.7484404
$lng = $place->location->getLng();  // -73.9878441

Geometry Classes

All geometry types implement a common interface and work identically on both MySQL and PostgreSQL.

Class OpenGIS Type
Point($lat, $lng, $srid = 0) Point
MultiPoint(Point[], $srid = 0) MultiPoint
LineString(Point[], $srid = 0) LineString
MultiLineString(LineString[], $srid = 0) MultiLineString
Polygon(LineString[], $srid = 0) Polygon
MultiPolygon(Polygon[], $srid = 0) MultiPolygon
GeometryCollection(Geometry[], $srid = 0) GeometryCollection

Working with Geometries

From/To WKT (Well Known Text)

use Habib\LaravelSpatial\Types\Point;
use Habib\LaravelSpatial\Types\Polygon;

$point = Point::fromWKT('POINT(2 1)');
echo $point->toWKT();  // POINT(2 1)

$polygon = Polygon::fromWKT('POLYGON((0 0,4 0,4 4,0 4,0 0),(1 1, 2 1, 2 2, 1 2,1 1))');
echo $polygon->toWKT();

From/To GeoJSON

use Habib\LaravelSpatial\Types\Point;
use Habib\LaravelSpatial\Types\Geometry;

$point = new Point(40.7484404, -73.9878441);
$json = json_encode($point);
// {
//   "type": "Feature",
//   "properties": {},
//   "geometry": {
//     "type": "Point",
//     "coordinates": [-73.9878441, 40.7484404]
//   }
// }

// Deserialize
$location = Geometry::fromJson('{"type":"Point","coordinates":[3.4,1.2]}');

Spatial Scopes

Query with spatial functions using Eloquent scopes. The same API works for both MySQL and PostgreSQL:

use App\Models\Place;
use Habib\LaravelSpatial\Types\Point;
use Habib\LaravelSpatial\Types\Polygon;

// Distance queries
$center = new Point(40.7484, -73.9878);
$nearby = Place::distance('location', $center, 1000)->get();

// Distance excluding the point itself
$others = Place::distanceExcludingSelf('location', $center, 5000)->get();

// Spherical distance (uses Earth's curvature)
$nearby = Place::distanceSphere('location', $center, 5000)->get();

// Geometric comparisons
$polygon = Polygon::fromWKT('POLYGON((0 0,0 5,5 5,5 0,0 0))');

Place::within('location', $polygon)->get();
Place::contains('area', $point)->get();
Place::intersects('area', $line)->get();
Place::crosses('line', $geometry)->get();
Place::disjoint('area', $geometry)->get();
Place::overlaps('area', $polygon)->get();
Place::equals('location', $point)->get();
Place::touches('area', $polygon)->get();

// Ordering by distance
$ordered = Place::orderByDistance('location', $center)->get();
$ordered = Place::orderByDistanceSphere('location', $center, 'desc')->get();

Available Scopes

  • distance($column, $geometry, $distance)
  • distanceExcludingSelf($column, $geometry, $distance)
  • distanceSphere($column, $geometry, $distance)
  • distanceSphereExcludingSelf($column, $geometry, $distance)
  • within($column, $polygon)
  • contains($column, $geometry)
  • crosses($column, $geometry)
  • disjoint($column, $geometry)
  • equals($column, $geometry)
  • intersects($column, $geometry)
  • overlaps($column, $geometry)
  • doesTouch($column, $geometry)
  • orderByDistance($column, $geometry, $direction = 'asc')
  • orderByDistanceSphere($column, $geometry, $direction = 'asc')

Migrations

Available Column Types

$table->geometry('column_name', $srid = 0);
$table->point('column_name', $srid = 0);
$table->lineString('column_name', $srid = 0);
$table->polygon('column_name', $srid = 0);
$table->multiPoint('column_name', $srid = 0);
$table->multiLineString('column_name', $srid = 0);
$table->multiPolygon('column_name', $srid = 0);
$table->geometryCollection('column_name', $srid = 0);

Spatial Indexes

Schema::table('places', function (Blueprint $table) {
    // Make column NOT NULL (required for spatial indexes)
    $table->point('location')->nullable(false)->change();
    
    // Add spatial index
    $table->spatialIndex('location');
});

// Drop spatial index
Schema::table('places', function (Blueprint $table) {
    $table->dropSpatialIndex(['location']);
    // or by index name
    $table->dropSpatialIndex('places_location_spatial');
});

Note: Spatial indexes require columns to be NOT NULL. For MySQL, InnoDB tables support spatial indexes (MySQL 5.7.5+). For PostgreSQL, GIST indexes are used automatically.

Database-Specific Considerations

While the API is identical, there are some internal differences:

MySQL 8+

  • Column definition: POINT SRID 4326
  • Function calls: ST_GeomFromText(?, ?, 'axis-order=long-lat')
  • Index type: SPATIAL INDEX

PostgreSQL + PostGIS

  • Column definition: geometry(Point, 4326)
  • Function calls: ST_GeomFromText(?, ?)
  • Index type: GIST INDEX
  • Requires PostGIS extension

The package handles these differences automatically, providing a unified API.

Testing

# Run all tests
composer test

# Unit tests only
composer test:unit

# Integration tests (requires database)
composer test:integration

Integration Test Setup

For MySQL:

docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=spatial_test mysql:8.0

For PostgreSQL:

docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=spatial_test postgis/postgis:16-3.4

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Credits

License

This package is open-sourced software licensed under the MIT license.