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
Requires
- php: ^8.2
- ext-json: *
- ext-pdo: *
- geo-io/wkb-parser: ^v1.0.2
- illuminate/database: ^9.0||^10.0||^11.0||^12.0
- jmikola/geojson: ^1.1.2
Requires (Dev)
- doctrine/dbal: ^3 || ^4
- laravel/browser-kit-testing: ^2.0
- laravel/laravel: ^9.0||^10.0||^11.0||^12.0
- laravel/pint: ^1 || ^2
- mockery/mockery: ^1.3
- phpunit/phpunit: ~10.3.1 || ~11.0 || ~12.0
This package is auto-updated.
Last update: 2025-12-06 22:17:38 UTC
README
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+
- Uses MySQL's native Spatial Data Types
- Leverages MySQL Spatial Functions
- SRID support with
POINT SRID 4326syntax - Spatial indexes with
SPATIAL INDEX
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
- Originally inspired by njbarrett's Laravel PostGIS package
- Original MySQL implementation by Joseph Estefane
- Multi-database support and PHP 8.2+ modernization
License
This package is open-sourced software licensed under the MIT license.