Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Magellan Laravel Package

clickbar/laravel-magellan

Laravel Magellan is a modern PostGIS toolbox for Laravel. It provides geometry data classes, generators and parsers, request validation, and seamless database interaction for inserting, updating, and querying spatial data with PostGIS functions.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements: PostgreSQL with the PostGIS extension (MySQL/SQLite are out), a recent Laravel + PHP (v2 targets modern Laravel/PHP — check the badge/composer.json before upgrading a legacy app).

Install in four steps:

composer require clickbar/laravel-magellan

# publishes config/magellan.php
php artisan vendor:publish --tag="magellan-config"

# publishes a migration that runs "CREATE EXTENSION postgis"
php artisan vendor:publish --tag="magellan-migrations"
php artisan migrate

Where to look first in the README: What's included (shows the four pillars: geometry data classes, generators/parsers, request validation, DB interaction), then Creating Tables with PostGIS Columns → Preparing the Model → Using the geometry data classes. Skip Generators & Parsers on day one — you only need it when talking to a frontend map or an import/export job.

First use case — store a point and read it back:

// migration
Schema::create('places', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->geometry('location', subtype: 'point', srid: 4326);
    $table->spatialIndex('location');   // don't skip this
    $table->timestamps();
});
use Clickbar\Magellan\Data\Geometries\Point;

class Place extends Model
{
    protected $casts = [
        'location' => Point::class,     // hydration + persistence handled for you
    ];
}

$place = Place::create([
    'name'     => 'Café Kiez',
    'location' => Point::makeGeodetic(52.5200, 13.4050), // lat, lng — geodetic
]);

That cast is the whole value proposition: $place->location is a Point object, $place->save() writes valid EWKT/WKB, and queries via a raw ST_AsText are no longer needed for the happy path.

Implementation Patterns

1. Cast every geometry column, and only geometry columns. Keep subtypes aligned with the DB (subtype: 'point' ↔ Point::class). A mismatch only surfaces at write time as a PostGIS type error. For nullable columns, the cast naturally yields null.

2. Validate GeoJSON at the edge, store geometry at the core. Mobile/web map widgets emit GeoJSON. Use the shipped request rules for shape validation (geometry type, dimension, coordinate sanity — see the Request Validation and Transformation section for the exact rule classes in your version) and let the transformation step hand you a geometry object, so the controller never touches strings:

public function store(StorePlaceRequest $request)
{
    return Place::create($request->validated()); // 'location' already a Point
}

If you'd rather parse manually, GeoJsonParser / WKTParser / WKBParser are the entry points, and the *Generator counterparts turn a geometry back into a string. Parsing is also the right tool for imports (GPX/KML jobs, CSV dumps) where you have no Eloquent model in play.

3. Serialize back to GeoJSON in API Resources rather than exposing EWKT. Generate GeoJSON once in toArray() on the resource so every endpoint returns the same shape:

public function toArray($request): array
{
    return [
        'id'       => $this->id,
        'name'     => $this->name,
        'location' => GeoJsonGenerator::generate($this->location), // or ->toGeoJson()
    ];
}

4. Nearby / containment searches. The package's Interaction with the database section documents the query-builder helpers; whether or not the helper exists for your predicate, the pattern is the same — build the search geometry once, bind it, and use an index-friendly predicate:

$searchPoint = Point::makeGeodetic($lat, $lng);

Place::query()
    ->whereRaw('ST_DWithin(location::geography, ?::geography, ?)', [
        Magellan::toSql($searchPoint),  // serialise the value object for a raw binding
        $radiusInMeters,
    ])
    ->orderByRaw('location <-> ?::geometry', [Magellan::toSql($searchPoint)]) // KNN
    ->limit(20)
    ->get();

Rule of thumb: ST_DWithin for filtering (uses the GiST index), ST_Distance only for ordering/display. Cast to geography when you want metres; on plain geometry in SRID 4326 a distance is in degrees and your "5 km" radius is silently wrong.

5. Keep SRID in config, not in your head. Put the project's SRID (and whether you're geodetic or projected) in config/magellan.php and reference it from factories, parsers and validation rules. Every parser/generator accepts an SRID override — use it explicitly when ingesting third-party data (e.g. Swiss LV95 / EPSG:2056) and let PostGIS reproject with ST_Transform.

6. Factories and seeders. Generate plausible fixtures with Point::makeGeodetic(...) inside a bounding box; keep a handful of fixed coordinates as named constants so assertions and snapshot tests are stable.

7. Testing. Point your phpunit.xml at a Postgres database with PostGIS enabled, and run RefreshDatabase. The published postgis migration must run before your own migrations — if you squash migrations or use migrate:fresh --seed in CI, verify the extension is created first.

Gotchas and Tips

  • Coordinate order is the classic bug. WKT/GeoJSON and therefore Point::make($x, $y) are X-then-Y; makeGeodetic($lat, $lng) is latitude-first. Mixing them puts Berlin in the Indian Ocean and you only notice on a map. Pick one convention per codebase and never mix helpers on the same column.
  • "Operation on mixed SRID geometries" is the error you'll see when a query geometry and a column have different SRIDs. Fix by building search geometries with the same SRID as the column, or ST_Transform one side. Never reproject inside a WHERE on an indexed column — the index is bypassed.
  • Geography vs geometry. ST_Distance on a 4326 geometry returns degrees. If you need metres, either declare the column as geography (subtype-aware migration helper) or cast ::geography at query time. Decide once; retrofitting is a migration + index rebuild.
  • Bulk inserts bypass casts. Place::insert([...]) and upsert() never invoke the geometry cast, so you'd be writing raw values. Either loop with create(), or construct the SQL literal yourself (Magellan::toSql($point) or the WKT/WKB generator) for CSV imports. Use DB::transaction() around the loop.
  • Empty and Z/M geometries. POINT Z, empty geometries, and 3D data from GPS tracks will parse, but validation rules with a fixed dimension will reject them. Check the coordinate dimension on your incoming payload before the rule fires, and store an empty collection rather than null for "no shape yet" only if the column is NOT NULL.
  • toSql() / toArray() truncation. Float precision survives the round-trip to PostGIS as doubles, but JSON encoders apply serialize_precision. If map pins look off, dump the raw ST_AsText(location) in tinker and compare.
  • Managed Postgres caveats. On RDS/Cloud SQL, CREATE EXTENSION postgis may need a privileged role or a parameter-group change. Also confirm the extension lands in the schema listed in your connection's search_path, otherwise PostGIS functions aren't found.
  • Spatial index ≠ free performance. GiST indexes work with ST_DWithin, && bounding-box operators and KNN <->; a WHERE ST_Contains(a, b) with a computed geometry on the left side may still sequence-scan. Combine a bounding-box prefilter (ST_MakeEnvelope or &&) with the exact predicate for large tables.
  • Debugging loop: in tinker, Place::first()->location shows the value object; Place::selectRaw('ST_AsText(location) as wkt')->first()->wkt shows what the DB actually holds; Place::where(...)->toSql() plus getBindings() shows what you're sending. Most Magellan issues are found in those three lines.
  • Extension points: the geometry value objects are plain classes — add your own accessor (getLat()/getLng() wrappers, a toLeaflet() array, a distance formatter) as a model accessor or an extension of the geometry class, rather than sprinkling ST_* calls through controllers. Likewise, put reusable spatial scopes on a SpatialQueries trait so the raw SQL lives in one file.
  • Upgrading 1.x → 2.x moved namespaces and changed constructor signatures; read the Upgrading from 1.x to 2.x section before a composer bump, and grep for the old Clickbar\Magellan\Geometry\... imports — a half-migrated codebase fails only when a cast is hydrated at runtime, which is exactly what tests won't cover if your suite mocks models.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log