jeorgy / laravel-postgis
A production-grade Laravel package for PostGIS spatial types and geospatial queries through Eloquent.
Requires
- php: ^8.4
- laravel/framework: ^12.0 || ^13.0
Requires (Dev)
- orchestra/testbench: ^10.0
- phpunit/phpunit: ^11.0
README
Biblioteca para consultas geoespaciais em Eloquent com PostgreSQL/PostGIS: cálculo de distâncias, filtros por polígonos, conversão GeoJSON/WKT e objetos de valor imutáveis — com API moderna, tipada e nativa ao Laravel.
Requisitos
- PHP >= 8.4
- Laravel ^12.0
- PostgreSQL com extensão PostGIS habilitada
CREATE EXTENSION IF NOT EXISTS postgis;
Instalação
Via Packagist (versão estável)
composer require jeorgy/laravel-postgis
Via repositório local (desenvolvimento)
Se você está testando a versão de desenvolvimento antes da publicação, adicione um repositório local no composer.json da sua aplicação Laravel:
{
"repositories": [
{
"type": "path",
"url": "/caminho/absoluto/para/laravel-postgis"
}
],
"require": {
"jeorgy/laravel-postgis": "*"
}
}
Em seguida:
composer require jeorgy/laravel-postgis:@dev
Via repositório Git
{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/jeorgy/laravel-postgis"
}
],
"require": {
"jeorgy/laravel-postgis": "dev-main"
}
}
O Service Provider é registrado automaticamente via Laravel Package Auto-Discovery.
Configuração do Modelo
Adicione a trait Postgis (ou HasSpatialColumns) ao seu modelo Eloquent que possui uma coluna geoespacial.
use Illuminate\Database\Eloquent\Model; use Jeorgy\LaravelPostgis\Postgis; class Place extends Model { use Postgis; }
Coluna personalizada
Por padrão a trait usa a coluna location. Para alterar, defina a propriedade $location:
class Place extends Model { use Postgis; protected $location = 'coordinates'; }
Unidade de distância
Use a propriedade $spatialUnit com o enum DistanceUnit para configurar a unidade retornada pelo escopo withDistance:
use Jeorgy\LaravelPostgis\Enums\DistanceUnit; class Place extends Model { use Postgis; protected DistanceUnit $spatialUnit = DistanceUnit::KILOMETER; }
| Enum | Unidade | Fator de divisão |
|---|---|---|
DistanceUnit::METER |
metros | 1 (padrão) |
DistanceUnit::KILOMETER |
quilômetros | 1000 |
DistanceUnit::MILE |
milhas | 1609.344 |
Migration
Garanta que a coluna geoespacial seja do tipo adequado ao PostGIS:
Schema::create('places', function (Blueprint $table) { $table->id(); $table->string('name'); $table->specificType('location', 'geography(Point, 4326)'); $table->timestamps(); });
Objetos de Valor
Point
Objeto imutável (final readonly class) que representa um ponto geográfico.
use Jeorgy\LaravelPostgis\Geometries\Point; $point = new Point(latitude: -23.5505, longitude: -46.6333); $point = new Point(latitude: -23.5505, longitude: -46.6333, srid: 4326); $point->latitude; // -23.5505 $point->longitude; // -46.6333 $point->srid; // 4326 $point->toWkt(); // "POINT(-46.6333 -23.5505)" $point->toGeoJson(); // ['type' => 'Point', 'coordinates' => [-46.6333, -23.5505]]
Validações aplicadas automaticamente na construção:
- Latitude deve estar entre
-90e90 - Longitude deve estar entre
-180e180 - SRID deve ser um inteiro positivo
Polygon
Objeto que representa um polígono a partir de anéis de coordenadas ou GeoJSON.
use Jeorgy\LaravelPostgis\Geometries\Polygon; // A partir de anéis de coordenadas [lng, lat] $polygon = new Polygon([ [[30, 10], [10, 20], [20, 40], [40, 40]], ]); // A partir de um objeto GeoJSON $geoJson = json_decode('{"geometry":{"coordinates":[[[30,10],[10,20],[20,40],[40,40]]]}}'); $polygon = Polygon::fromGeoJson($geoJson); $polygon->toWkt(); // "POLYGON((30 10, 10 20, 20 40, 40 40, 30 10))" $polygon->toGeoJson(); // ['type' => 'Polygon', 'coordinates' => [...]]
O anel é fechado automaticamente no WKT se o primeiro e último ponto diferirem.
Escopos de Consulta
withDistance
Adiciona uma coluna distance ao resultado usando ST_DistanceSphere. O valor retornado respeita a $spatialUnit do modelo.
$origin = new Point(latitude: -23.5505, longitude: -46.6333); $places = Place::query() ->withDistance($origin) ->orderBy('distance') ->get(); // $place->distance => distância em metros (ou km/milhas conforme $spatialUnit)
Passando null, a coluna distance retorna 0:
Place::query()->withDistance(null)->get();
Também aceita array [lat, lng] ou array associativo ['lat' => ..., 'lng' => ...]:
Place::query()->withDistance([-23.5505, -46.6333])->get(); Place::query()->withDistance(['lat' => -23.5505, 'lng' => -46.6333])->get();
whereDistance / orWhereDistance
Filtra registros por distância com um operador de comparação. O valor de $units é sempre em metros (independente da $spatialUnit).
use Jeorgy\LaravelPostgis\Geometries\Point; $origin = new Point(latitude: -23.5505, longitude: -46.6333); // Registros a menos de 5 km (5000 metros) Place::query() ->whereDistance($origin, '<', 5000) ->get(); // Combinando com orWhereDistance Place::query() ->whereDistance($origin, '<', 1000) ->orWhereDistance($origin, '>', 10000) ->get();
Operadores válidos: <, <=, >, >=, =
Operadores inválidos lançam SpatialQueryException.
whereCovers / orWhereCovers
Filtra registros cujo ponto geoespacial está dentro de um polígono, usando ST_Covers.
use Jeorgy\LaravelPostgis\Geometries\Polygon; // A partir de um objeto Polygon $polygon = new Polygon([ [[-46.7, -23.6], [-46.5, -23.6], [-46.5, -23.4], [-46.7, -23.4]], ]); Place::query()->whereCovers($polygon)->get(); // A partir de um objeto GeoJSON $geoJson = json_decode('{"geometry":{"coordinates":[[[30,10],[10,20],[20,40],[40,40]]]}}'); Place::query() ->where('active', true) ->whereCovers($geoJson) ->orWhereCovers($outroGeoJson) ->get();
Cast de Ponto
Para que o Eloquent hidrate e persista automaticamente a coluna como um objeto Point:
use Jeorgy\LaravelPostgis\Casts\PointCast; class Place extends Model { use Postgis; protected $casts = [ 'location' => PointCast::class, ]; } // Hidratação automática $place = Place::find(1); $place->location; // instância de Point $place->location->latitude; // float
Exemplo completo
use Jeorgy\LaravelPostgis\Enums\DistanceUnit; use Jeorgy\LaravelPostgis\Geometries\Point; use Jeorgy\LaravelPostgis\Geometries\Polygon; use Jeorgy\LaravelPostgis\Postgis; class Place extends Model { use Postgis; protected DistanceUnit $spatialUnit = DistanceUnit::KILOMETER; protected $casts = [ 'location' => \Jeorgy\LaravelPostgis\Casts\PointCast::class, ]; } $origin = new Point(latitude: -23.5505, longitude: -46.6333); $nearby = Place::query() ->withDistance($origin) ->whereDistance($origin, '<', 5000) ->orderBy('distance') ->limit(10) ->get(); $area = new Polygon([ [[-46.7, -23.6], [-46.5, -23.6], [-46.5, -23.4], [-46.7, -23.4]], ]); $inside = Place::query() ->whereCovers($area) ->get();
Exceções
Todas as exceções do pacote estendem SpatialException:
| Exceção | Quando é lançada |
|---|---|
InvalidCoordinatesException |
Latitude/longitude fora do intervalo válido |
InvalidSridException |
SRID não positivo |
InvalidGeometryException |
Polígono malformado, anel com menos de 3 pontos, etc. |
UnsupportedGeometryException |
Tipo de geometria não suportado no GeoJSON |
SpatialQueryException |
Operador inválido ou representação de ponto inválida |
use Jeorgy\LaravelPostgis\Exceptions\SpatialException; try { $results = Place::query()->whereDistance($point, '!=', 1000)->get(); } catch (SpatialException $e) { // Trate erros espaciais de forma unificada logger()->error('Spatial error', ['message' => $e->getMessage()]); }
Testes
vendor/bin/phpunit