/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
- Cache geocoding results - Google Maps API has rate limits and costs
- Validate coordinates - Always check latitude (-90 to 90) and longitude (-180 to 180) ranges
- Use spatial indexes - Index latitude and longitude columns for performance
- Handle errors gracefully - Geocoding can fail for invalid or ambiguous addresses
- Batch geocoding - When geocoding multiple addresses, batch requests when possible
- Store formatted addresses - Save the formatted address returned by Google
- Consider alternative providers - Have fallback geocoding providers for redundancy
Setup Steps
- Get Google Maps API key from Google Cloud Console
- Enable Geocoding API in Google Cloud Console
- Add
GOOGLE_MAPS_GEOCODING_API_KEYto your.envfile - Run
php artisan migrateto create the locations table - 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
- /laravel-agent:api:make - Create API endpoints for geolocation features
- /laravel-agent:test:make - Generate tests for geocoding features
- Spatie Geocoder Documentation - Official package documentation