mohamedhabibwork/laravel-spatial

Multi-database spatial data types extension for Laravel supporting MySQL 8+ and PostgreSQL 16+ (with PostGIS).

Installs: 0

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/mohamedhabibwork/laravel-spatial

dev-main 2025-12-06 22:15 UTC

This package is auto-updated.

Last update: 2025-12-06 22:17:38 UTC


README

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.