Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save imrankabir02/c6153e1969eb260c9db97c19e10a280d to your computer and use it in GitHub Desktop.

Select an option

Save imrankabir02/c6153e1969eb260c9db97c19e10a280d to your computer and use it in GitHub Desktop.
Laravel PostgreSQL Geospatial Implementation Guide

Laravel PostgreSQL Geospatial Implementation Guide

This guide provides a comprehensive walkthrough for implementing geospatial functionality in a Laravel application using PostgreSQL with PostGIS.

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Setting Up PostgreSQL with PostGIS
  4. Configuring Laravel for Spatial Data
  5. Creating Migrations with Spatial Columns
  6. Setting Up Eloquent Models
  7. Implementing Core Geospatial Techniques
  8. Frontend Integration
  9. Performance Optimization
  10. Common Issues and Solutions
  11. Useful Extensions and Packages

Introduction

Geospatial functionality allows your application to work with location data, enabling features like:

  • Finding nearby locations
  • Determining if a point is within a defined area
  • Calculating distances between points
  • Creating geofences for location-based triggers

PostgreSQL, combined with its PostGIS extension, provides powerful geospatial capabilities that integrate well with Laravel.

Prerequisites

  • PHP 8.0+
  • Laravel 8.0+
  • PostgreSQL 12+
  • PostGIS 3.0+
  • Composer

Setting Up PostgreSQL with PostGIS

1. Install PostgreSQL and PostGIS

For Ubuntu/Debian:

sudo apt update
sudo apt install postgresql postgresql-contrib
sudo apt install postgis postgresql-12-postgis-3

For macOS with Homebrew:

brew install postgresql
brew install postgis

2. Create a Database for Your Laravel Application

sudo -u postgres psql
CREATE DATABASE laravel_geo;
CREATE USER laravel_user WITH ENCRYPTED PASSWORD 'your_password';
GRANT ALL PRIVILEGES ON DATABASE laravel_geo TO laravel_user;
\c laravel_geo

3. Enable PostGIS Extension

After connecting to your database:

CREATE EXTENSION postgis;
CREATE EXTENSION postgis_topology;

To verify PostGIS is installed correctly:

SELECT PostGIS_Version();

Configuring Laravel for Spatial Data

1. Install Laravel and Set Up Database Connection

Create a new Laravel project if you don't have one:

composer create-project laravel/laravel your-project-name
cd your-project-name

Update your .env file with PostgreSQL connection details:

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=laravel_geo
DB_USERNAME=laravel_user
DB_PASSWORD=your_password

2. Install Required Packages

There are several packages that help with geospatial functionality in Laravel:

composer require grimzy/laravel-mysql-spatial

While this package has "mysql" in its name, it works with PostgreSQL as well.

Alternatively, you can use:

composer require matanyadaev/laravel-eloquent-spatial

Creating Migrations with Spatial Columns

Create migrations for your location-based tables:

php artisan make:migration create_locations_table
php artisan make:migration create_geofences_table

Update the Locations Migration

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

class CreateLocationsTable extends Migration
{
    public function up()
    {
        Schema::create('locations', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->timestamps();
        });

        // Add Point column with PostGIS
        DB::statement('ALTER TABLE locations ADD COLUMN coordinates GEOMETRY(Point, 4326)');
        
        // Add spatial index
        DB::statement('CREATE INDEX locations_coordinates_idx ON locations USING GIST (coordinates)');
    }

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

Update the Geofences Migration

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

class CreateGeofencesTable extends Migration
{
    public function up()
    {
        Schema::create('geofences', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->timestamps();
        });

        // Add Polygon column with PostGIS
        DB::statement('ALTER TABLE geofences ADD COLUMN boundary GEOMETRY(Polygon, 4326)');
        
        // Add spatial index
        DB::statement('CREATE INDEX geofences_boundary_idx ON geofences USING GIST (boundary)');
    }

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

Run your migrations:

php artisan migrate

Setting Up Eloquent Models

Location Model

php artisan make:model Location

Update the model with spatial capabilities:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Grimzy\LaravelMysqlSpatial\Eloquent\SpatialTrait;

class Location extends Model
{
    use HasFactory, SpatialTrait;

    protected $fillable = [
        'name',
        'description',
        'coordinates'
    ];

    protected $spatialFields = [
        'coordinates',
    ];
}

Geofence Model

php artisan make:model Geofence

Update the model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Grimzy\LaravelMysqlSpatial\Eloquent\SpatialTrait;

class Geofence extends Model
{
    use HasFactory, SpatialTrait;

    protected $fillable = [
        'name',
        'description',
        'boundary'
    ];

    protected $spatialFields = [
        'boundary',
    ];
}

Implementing Core Geospatial Techniques

Creating Controllers

php artisan make:controller LocationController
php artisan make:controller GeofenceController

LocationController.php

<?php

namespace App\Http\Controllers;

use App\Models\Location;
use App\Models\Geofence;
use Illuminate\Http\Request;
use Grimzy\LaravelMysqlSpatial\Types\Point;

class LocationController extends Controller
{
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'latitude' => 'required|numeric',
            'longitude' => 'required|numeric',
        ]);

        $location = Location::create([
            'name' => $request->name,
            'description' => $request->description,
            'coordinates' => new Point($request->latitude, $request->longitude),
        ]);

        return response()->json($location, 201);
    }

    public function nearby(Request $request)
    {
        $request->validate([
            'latitude' => 'required|numeric',
            'longitude' => 'required|numeric',
            'distance' => 'required|numeric', // in meters
        ]);

        $latitude = $request->latitude;
        $longitude = $request->longitude;
        $distance = $request->distance;

        // Find locations within specified distance
        $locations = Location::whereRaw("ST_DWithin(
            coordinates::geography, 
            ST_SetSRID(ST_Point(?, ?), 4326)::geography, 
            ?
        )", [$longitude, $latitude, $distance])
        ->get();

        return response()->json($locations);
    }

    public function checkInGeofence(Request $request)
    {
        $request->validate([
            'latitude' => 'required|numeric',
            'longitude' => 'required|numeric',
        ]);

        $latitude = $request->latitude;
        $longitude = $request->longitude;

        // Find all geofences that contain this point
        $geofences = Geofence::whereRaw("ST_Contains(
            boundary, 
            ST_SetSRID(ST_Point(?, ?), 4326)
        )", [$longitude, $latitude])
        ->get();

        return response()->json([
            'in_geofence' => $geofences->isNotEmpty(),
            'geofences' => $geofences
        ]);
    }
}

GeofenceController.php

<?php

namespace App\Http\Controllers;

use App\Models\Geofence;
use Illuminate\Http\Request;
use Grimzy\LaravelMysqlSpatial\Types\Polygon;
use Grimzy\LaravelMysqlSpatial\Types\LineString;
use Grimzy\LaravelMysqlSpatial\Types\Point;

class GeofenceController extends Controller
{
    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'description' => 'nullable|string',
            'coordinates' => 'required|array',
            'coordinates.*' => 'required|array',
            'coordinates.*.*' => 'required|numeric',
        ]);

        // Convert the coordinates to a polygon
        $points = [];
        foreach ($request->coordinates as $coord) {
            $points[] = new Point($coord[1], $coord[0]); // lat, lng
        }
        
        // Add the first point again to close the polygon
        if ($points[0]->getLat() !== end($points)->getLat() || 
            $points[0]->getLng() !== end($points)->getLng()) {
            $points[] = $points[0];
        }

        $lineString = new LineString($points);
        $polygon = new Polygon([$lineString]);

        $geofence = Geofence::create([
            'name' => $request->name,
            'description' => $request->description,
            'boundary' => $polygon,
        ]);

        return response()->json($geofence, 201);
    }

    public function getLocationsInGeofence($geofenceId)
    {
        $geofence = Geofence::findOrFail($geofenceId);

        $locations = Location::whereRaw("ST_Contains(?, coordinates)", [$geofence->boundary])
            ->get();

        return response()->json([
            'geofence' => $geofence,
            'locations' => $locations
        ]);
    }
}

Set Up Routes

In your routes/api.php file:

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\LocationController;
use App\Http\Controllers\GeofenceController;

// Location routes
Route::post('/locations', [LocationController::class, 'store']);
Route::get('/locations/nearby', [LocationController::class, 'nearby']);
Route::get('/locations/in-geofence', [LocationController::class, 'checkInGeofence']);

// Geofence routes
Route::post('/geofences', [GeofenceController::class, 'store']);
Route::get('/geofences/{geofence}/locations', [GeofenceController::class, 'getLocationsInGeofence']);

Implementing Core Geospatial Techniques

Point-in-Polygon Queries

This technique determines if a geographic point (latitude, longitude) is inside a polygon area.

// In a controller or service:
public function isPointInArea($lat, $lng, $geofenceId)
{
    $geofence = Geofence::findOrFail($geofenceId);
    
    $isInside = DB::select("
        SELECT ST_Contains(
            boundary, 
            ST_SetSRID(ST_Point(?, ?), 4326)
        ) as is_inside
        FROM geofences
        WHERE id = ?
    ", [$lng, $lat, $geofenceId])[0]->is_inside;
    
    return $isInside;
}

Geofencing

Geofencing allows you to trigger actions when a point enters or exits a defined area.

// Check if a point is inside any registered geofence
public function checkGeofences($lat, $lng) 
{
    $matchingGeofences = Geofence::whereRaw("
        ST_Contains(
            boundary, 
            ST_SetSRID(ST_Point(?, ?), 4326)
        )", [$lng, $lat]
    )->get();
    
    if ($matchingGeofences->isNotEmpty()) {
        // Point is inside at least one geofence
        // Trigger notification or action
        foreach ($matchingGeofences as $fence) {
            event(new EnteredGeofence($fence->id, $lat, $lng));
        }
    }
    
    return $matchingGeofences;
}

Distance-Based Queries

Find locations within a certain distance of a point:

// Get all locations within a specified distance
public function getNearbyLocations($lat, $lng, $distanceInMeters) 
{
    return Location::whereRaw("
        ST_DWithin(
            coordinates::geography, 
            ST_SetSRID(ST_Point(?, ?), 4326)::geography, 
            ?
        )", [$lng, $lat, $distanceInMeters]
    )->get();
}

// Calculate distance between two points
public function getDistance($lat1, $lng1, $lat2, $lng2) 
{
    // Returns distance in meters
    $result = DB::select("
        SELECT ST_Distance(
            ST_SetSRID(ST_Point(?, ?), 4326)::geography,
            ST_SetSRID(ST_Point(?, ?), 4326)::geography
        ) as distance", [$lng1, $lat1, $lng2, $lat2]
    );
    
    return $result[0]->distance;
}

Location Indexing

To ensure good performance with large datasets, spatial indexing is crucial:

// This was already included in our migrations, but here's the manual approach:
DB::statement('CREATE INDEX locations_coordinates_idx ON locations USING GIST (coordinates)');
DB::statement('CREATE INDEX geofences_boundary_idx ON geofences USING GIST (boundary)');

Frontend Integration

API Endpoints

Set up RESTful API endpoints for your geospatial features:

// In routes/api.php
Route::get('/locations/nearby', [LocationController::class, 'nearby']);
Route::post('/locations/check-geofence', [LocationController::class, 'checkInGeofence']);
Route::get('/locations/{location}/distance/{otherLocation}', [LocationController::class, 'getDistanceBetweenLocations']);

Example JavaScript for Browser Geolocation

// Get current location from browser
function getCurrentPosition() {
    return new Promise((resolve, reject) => {
        if (!navigator.geolocation) {
            reject(new Error('Geolocation is not supported by your browser'));
        } else {
            navigator.geolocation.getCurrentPosition(
                position => {
                    resolve({
                        latitude: position.coords.latitude,
                        longitude: position.coords.longitude
                    });
                },
                () => {
                    reject(new Error('Unable to retrieve your location'));
                }
            );
        }
    });
}

// Check if user is in a geofence
async function checkGeofence() {
    try {
        const position = await getCurrentPosition();
        const response = await fetch('/api/locations/check-geofence', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
            },
            body: JSON.stringify({
                latitude: position.latitude,
                longitude: position.longitude
            })
        });
        
        const data = await response.json();
        if (data.in_geofence) {
            console.log('You are in these geofences:', data.geofences);
        } else {
            console.log('You are not in any geofence.');
        }
    } catch (error) {
        console.error('Error:', error);
    }
}

Mapping Integration with Leaflet.js

<!-- In your Blade template -->
<div id="map" style="height: 500px;"></div>

<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />

<script>
    // Initialize map
    const map = L.map('map').setView([51.505, -0.09], 13);
    
    // Add tile layer
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '© OpenStreetMap contributors'
    }).addTo(map);
    
    // Load locations from API
    async function loadLocations() {
        const response = await fetch('/api/locations');
        const locations = await response.json();
        
        locations.forEach(location => {
            L.marker([location.coordinates.lat, location.coordinates.lng])
                .addTo(map)
                .bindPopup(location.name);
        });
    }
    
    // Load geofences from API
    async function loadGeofences() {
        const response = await fetch('/api/geofences');
        const geofences = await response.json();
        
        geofences.forEach(geofence => {
            const coordinates = geofence.boundary.coordinates[0].map(coord => [coord.lat, coord.lng]);
            
            L.polygon(coordinates, {color: 'red'})
                .addTo(map)
                .bindPopup(geofence.name);
        });
    }
    
    loadLocations();
    loadGeofences();
</script>

Performance Optimization

Optimizing Spatial Queries

  1. Always use spatial indexes:
CREATE INDEX idx_locations_coordinates ON locations USING GIST (coordinates);
  1. For frequent distance calculations, consider a materialized view:
CREATE MATERIALIZED VIEW common_distances AS
SELECT 
    l1.id as location1_id,
    l2.id as location2_id,
    ST_Distance(l1.coordinates::geography, l2.coordinates::geography) as distance
FROM locations l1, locations l2
WHERE l1.id < l2.id;

-- Refresh when needed
REFRESH MATERIALIZED VIEW common_distances;
  1. Use the ::geography type for accurate earth distance calculations:
$locations = Location::whereRaw("
    ST_DWithin(
        coordinates::geography, 
        ST_SetSRID(ST_Point(?, ?), 4326)::geography, 
        ?
    )", [$lng, $lat, $distance]
)->get();

Caching Strategies

For frequently requested queries:

// In a service class
public function getNearbyLocations($lat, $lng, $distance)
{
    $cacheKey = "nearby_locations_{$lat}_{$lng}_{$distance}";
    
    return Cache::remember($cacheKey, now()->addMinutes(15), function () use ($lat, $lng, $distance) {
        return Location::whereRaw("
            ST_DWithin(
                coordinates::geography, 
                ST_SetSRID(ST_Point(?, ?), 4326)::geography, 
                ?
            )", [$lng, $lat, $distance]
        )->get();
    });
}

Common Issues and Solutions

Problem: Slow Queries on Large Datasets

Solution:

  • Ensure spatial indexes are created
  • Use ST_DWithin instead of calculating distance for each record
  • Consider clustering your data based on geographic regions

Problem: Incorrect Distance Calculations

Solution:

  • Always use ::geography type for distance calculations on Earth
  • Make sure coordinates are in the correct format (longitude, latitude) for ST_Point
// Correct way for earth distance calculation
$distance = DB::select("
    SELECT ST_Distance(
        ST_SetSRID(ST_Point(?, ?), 4326)::geography,
        ST_SetSRID(ST_Point(?, ?), 4326)::geography
    ) as distance", [$lng1, $lat1, $lng2, $lat2]
)[0]->distance;

Problem: Points Not Found Inside Polygons

Solution:

  • Check if polygons are defined clockwise
  • Verify polygon is closed (first and last points match)
  • Ensure coordinates are in the correct order (longitude, latitude) for PostGIS functions

Useful Extensions and Packages

Additional Laravel Packages

  1. Laravel GeoJSON: Add GeoJSON support for Laravel models
composer require matanyadaev/laravel-eloquent-spatial
  1. Laravel Geocoder: Add geocoding capabilities
composer require spatie/laravel-geocoder

PostGIS Functions Worth Exploring

  • ST_AsGeoJSON - Convert geometry to GeoJSON
  • ST_Buffer - Create a buffer around a geometry
  • ST_Intersection - Find where geometries intersect
  • ST_Union - Combine multiple geometries
  • ST_Simplify - Simplify geometries (useful for complex polygons)

Example: Converting to GeoJSON

// In a controller
public function getGeoJson()
{
    $locations = DB::select("
        SELECT 
            id, 
            name, 
            ST_AsGeoJSON(coordinates) as geometry 
        FROM locations
    ");
    
    $features = [];
    foreach ($locations as $location) {
        $geometry = json_decode($location->geometry);
        $features[] = [
            'type' => 'Feature',
            'properties' => [
                'id' => $location->id,
                'name' => $location->name
            ],
            'geometry' => $geometry
        ];
    }
    
    $geoJson = [
        'type' => 'FeatureCollection',
        'features' => $features
    ];
    
    return response()->json($geoJson);
}

Conclusion

This guide has provided a comprehensive walkthrough for implementing geospatial functionality in a Laravel application using PostgreSQL with PostGIS. By leveraging these powerful tools, you can build location-aware applications that efficiently handle complex spatial operations.

Always remember:

  • Use spatial indexes for performance
  • Convert to geography type for accurate Earth calculations
  • Structure your data to take advantage of PostGIS's capabilities
  • Cache results from expensive spatial queries

For more advanced use cases, explore the full range of PostGIS functions and consider scaling options like database sharding based on geographic regions if your application needs to handle extremely large datasets.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment