Data

/laravel-agent:geo:make

Create geolocation features with Google Maps integration

Overview

The /geo:make command generates location-based features using spatie/geocoder for Google Maps geocoding. It creates a complete geolocation system including geocoding services, location models with spatial queries, and controllers for store locators, delivery zones, and nearby searches.

Usage

/laravel-agent:geo:make [feature name]

Examples

# Create a store locator feature
/laravel-agent:geo:make StoreLocator

# Create delivery zones with geofencing
/laravel-agent:geo:make DeliveryZones

# Create address autocomplete
/laravel-agent:geo:make AddressAutocomplete

# Create nearby search functionality
/laravel-agent:geo:make NearbySearch

What Gets Created

A complete geolocation feature includes the following components:

Component Location Description
Geocoding Service app/Services/GeocodingService.php Service for address geocoding and reverse geocoding
Location Model app/Models/Location.php Model with spatial queries and distance calculations
Feature Controller app/Http/Controllers/ Controller for the specific geolocation feature
Migration database/migrations/ Database schema with spatial indexes
Configuration config/geocoder.php Geocoder configuration file

Package Installation

The command automatically installs and configures the required package:

# Install spatie/geocoder
composer require spatie/geocoder

# Publish configuration
php artisan vendor:publish --provider="Spatie\Geocoder\GeocoderServiceProvider" --tag="config"

Environment Setup

You'll need to configure your Google Maps API key:

# Add to .env
GOOGLE_MAPS_GEOCODING_API_KEY=your-api-key

Generated Files Structure

For /laravel-agent:geo:make StoreLocator:

app/
├── Services/
│   └── GeocodingService.php
├── Models/
│   └── Location.php (or Store.php)
├── Http/
│   └── Controllers/
│       └── StoreLocatorController.php
config/
└── geocoder.php
database/migrations/
└── xxxx_create_locations_table.php

Geocoding Service

The generated service provides comprehensive geocoding functionality:

<?php

namespace App\Services;

use Spatie\Geocoder\Geocoder;

final class GeocodingService
{
    public function __construct(
        private readonly Geocoder $geocoder,
    ) {}

    /**
     * Get coordinates from address.
     *
     * @return array{lat: float, lng: float, accuracy: string, formatted_address: string}|null
     */
    public function geocode(string $address): ?array
    {
        $result = $this->geocoder->getCoordinatesForAddress($address);

        if ($result['lat'] === 0 && $result['lng'] === 0) {
            return null;
        }

        return $result;
    }

    /**
     * Get address from coordinates.
     *
     * @return array{address: string, street_number: string, route: string, city: string, state: string, country: string, postal_code: string}|null
     */
    public function reverseGeocode(float $lat, float $lng): ?array
    {
        $results = $this->geocoder->getAddressForCoordinates($lat, $lng);

        if (empty($results)) {
            return null;
        }

        return $this->parseAddressComponents($results);
    }

    /**
     * Get multiple results for ambiguous address.
     */
    public function geocodeMultiple(string $address): array
    {
        return $this->geocoder->getAllCoordinatesForAddress($address);
    }

    /**
     * Calculate distance between two points in kilometers.
     */
    public function calculateDistance(float $lat1, float $lng1, float $lat2, float $lng2): float
    {
        $earthRadius = 6371; // km

        $latDiff = deg2rad($lat2 - $lat1);
        $lngDiff = deg2rad($lng2 - $lng1);

        $a = sin($latDiff / 2) * sin($latDiff / 2) +
            cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
            sin($lngDiff / 2) * sin($lngDiff / 2);

        $c = 2 * atan2(sqrt($a), sqrt(1 - $a));

        return $earthRadius * $c;
    }
}

Location Model with Spatial Queries

The model includes powerful location-based query scopes:

<?php

namespace App\Models;

use App\Services\GeocodingService;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

final class Location extends Model
{
    protected $guarded = ['id'];

    protected $casts = [
        'latitude' => 'float',
        'longitude' => 'float',
    ];

    /**
     * Geocode address before saving.
     */
    protected static function booted(): void
    {
        static::saving(function (Location $location) {
            if ($location->isDirty('address') && !$location->latitude) {
                $geocoder = app(GeocodingService::class);
                $result = $geocoder->geocode($location->address);

                if ($result) {
                    $location->latitude = $result['lat'];
                    $location->longitude = $result['lng'];
                    $location->formatted_address = $result['formatted_address'];
                }
            }
        });
    }

    /**
     * Find locations within radius (km).
     */
    public function scopeWithinRadius(Builder $query, float $lat, float $lng, float $radiusKm): Builder
    {
        // Haversine formula
        return $query->selectRaw("
            *, (
                6371 * acos(
                    cos(radians(?)) * cos(radians(latitude)) *
                    cos(radians(longitude) - radians(?)) +
                    sin(radians(?)) * sin(radians(latitude))
                )
            ) AS distance
        ", [$lat, $lng, $lat])
        ->having('distance', '<=', $radiusKm)
        ->orderBy('distance');
    }

    /**
     * Find nearest location.
     */
    public function scopeNearest(Builder $query, float $lat, float $lng, int $limit = 10): Builder
    {
        return $query->selectRaw("
            *, (
                6371 * acos(
                    cos(radians(?)) * cos(radians(latitude)) *
                    cos(radians(longitude) - radians(?)) +
                    sin(radians(?)) * sin(radians(latitude))
                )
            ) AS distance
        ", [$lat, $lng, $lat])
        ->orderBy('distance')
        ->limit($limit);
    }

    /**
     * Get distance to another location.
     */
    public function distanceTo(Location $other): float
    {
        return app(GeocodingService::class)->calculateDistance(
            $this->latitude,
            $this->longitude,
            $other->latitude,
            $other->longitude
        );
    }
}

Store Locator Controller Example

Example controller for a store locator feature:

<?php

namespace App\Http\Controllers;

use App\Models\Store;
use App\Services\GeocodingService;
use Illuminate\Http\Request;

final class StoreLocatorController extends Controller
{
    public function __construct(
        private readonly GeocodingService $geocoder,
    ) {}

    public function index(Request $request)
    {
        $stores = Store::query()
            ->when($request->filled(['lat', 'lng']), function ($query) use ($request) {
                $query->withinRadius(
                    $request->float('lat'),
                    $request->float('lng'),
                    $request->float('radius', 50) // Default 50km
                );
            })
            ->when($request->filled('address'), function ($query) use ($request) {
                $coords = $this->geocoder->geocode($request->input('address'));
                if ($coords) {
                    $query->withinRadius($coords['lat'], $coords['lng'], 50);
                }
            })
            ->paginate(20);

        return view('stores.locator', compact('stores'));
    }

    public function nearest(Request $request)
    {
        $request->validate([
            'lat' => 'required|numeric|between:-90,90',
            'lng' => 'required|numeric|between:-180,180',
        ]);

        $stores = Store::nearest(
            $request->float('lat'),
            $request->float('lng'),
            5
        )->get();

        return response()->json([
            'data' => $stores->map(fn ($store) => [
                'id' => $store->id,
                'name' => $store->name,
                'address' => $store->formatted_address,
                'distance_km' => round($store->distance, 2),
                'lat' => $store->latitude,
                'lng' => $store->longitude,
            ]),
        ]);
    }
}

Database Migration

The migration includes spatial indexes for efficient proximity queries:

<?php

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('locations', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('address');
            $table->string('formatted_address')->nullable();
            $table->decimal('latitude', 10, 8)->nullable();
            $table->decimal('longitude', 11, 8)->nullable();
            $table->string('city')->nullable();
            $table->string('state', 10)->nullable();
            $table->string('postal_code', 20)->nullable();
            $table->string('country', 2)->nullable();
            $table->timestamps();

            // Spatial index for fast proximity queries
            $table->index(['latitude', 'longitude']);
        });
    }

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

Available Methods

After generation, you can use these methods in your application:

// Geocode address to coordinates
$coords = $geocoder->geocode('123 Main St, City, Country');
// Returns: ['lat' => 40.7128, 'lng' => -74.0060, 'accuracy' => 'ROOFTOP', 'formatted_address' => '...']

// Reverse geocode coordinates to address
$address = $geocoder->reverseGeocode(40.7128, -74.0060);
// Returns: ['address' => '...', 'city' => 'New York', 'state' => 'NY', ...]

// Get multiple results for ambiguous addresses
$results = $geocoder->geocodeMultiple('Springfield');

// Find locations within radius
$stores = Store::withinRadius($lat, $lng, 10)->get(); // 10km radius

// Find nearest locations
$stores = Store::nearest($lat, $lng, 5)->get(); // 5 closest stores

// Calculate distance between locations
$distance = $location1->distanceTo($location2); // Returns distance in km

Configuration

The generated configuration file:

// config/geocoder.php
return [
    'key' => env('GOOGLE_MAPS_GEOCODING_API_KEY'),
    'language' => 'en',
    'region' => 'us',
];

Common Use Cases

  • Store Locator - Find nearest stores based on user location
  • Delivery Zones - Validate addresses within service areas
  • Address Autocomplete - Provide address suggestions as users type
  • Geofencing - Create virtual boundaries and trigger actions
  • Distance Calculation - Calculate delivery distances and fees
  • Map Integration - Display locations on interactive maps
  • Location-Based Search - Filter results by proximity

Best Practices

  1. Cache geocoding results - Google Maps API has rate limits and costs
  2. Validate coordinates - Always check latitude (-90 to 90) and longitude (-180 to 180) ranges
  3. Use spatial indexes - Index latitude and longitude columns for performance
  4. Handle errors gracefully - Geocoding can fail for invalid or ambiguous addresses
  5. Batch geocoding - When geocoding multiple addresses, batch requests when possible
  6. Store formatted addresses - Save the formatted address returned by Google
  7. Consider alternative providers - Have fallback geocoding providers for redundancy

Setup Steps

  1. Get Google Maps API key from Google Cloud Console
  2. Enable Geocoding API in Google Cloud Console
  3. Add GOOGLE_MAPS_GEOCODING_API_KEY to your .env file
  4. Run php artisan migrate to create the locations table
  5. Test geocoding with a sample address

Spatial Query Performance

The Haversine formula used in the query scopes is accurate and works well for most use cases. For extremely high-performance requirements with millions of locations, consider:

  • Using MySQL spatial data types (POINT, GEOMETRY)
  • Implementing bounding box pre-filtering
  • Using dedicated geospatial databases (PostGIS, MongoDB with geospatial indexes)
  • Caching nearby results with Redis

See Also