martin-georgiev/postgresql-for-doctrine
Adds PostgreSQL-specific power to Doctrine DBAL/ORM: rich native types (jsonb, arrays, ranges, network, geometric, etc.) plus DQL functions/operators for JSON and array querying. Supports PostgreSQL 9.4+ and PHP 8.2+.
This document explains the usage, limitations, and workarounds for PostgreSQL geometry and geography array types in Doctrine DBAL.
📖 See also: PostGIS Spatial Functions and Operators for spatial functions that work with geometry and geography data
The GeometryArray and GeographyArray types provide support for PostgreSQL's GEOMETRY[] and GEOGRAPHY[] array types, allowing you to store collections of spatial data in a single database column. The use of these types currently has several limitations due to Doctrine DBAL's parameter binding behavior. Workarounds are provided for multi-item arrays in USE-CASES-AND-EXAMPLES.md.
use Doctrine\DBAL\Types\Type;
Type::addType('geometry', \MartinGeorgiev\Doctrine\DBAL\Types\Geometry::class);
Type::addType('geometry[]', \MartinGeorgiev\Doctrine\DBAL\Types\GeometryArray::class);
Type::addType('geography', \MartinGeorgiev\Doctrine\DBAL\Types\Geography::class);
Type::addType('geography[]', \MartinGeorgiev\Doctrine\DBAL\Types\GeographyArray::class);
$platform = $connection->getDatabasePlatform();
$platform->registerDoctrineTypeMapping('geometry', 'geometry');
$platform->registerDoctrineTypeMapping('_geometry', 'geometry[]');
$platform->registerDoctrineTypeMapping('geography', 'geography');
$platform->registerDoctrineTypeMapping('_geography', 'geography[]');
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\WktSpatialData;
use Doctrine\ORM\Mapping as ORM;
class Location
{
/**
* [@var](https://github.com/var) WktSpatialData[]
* [@ORM](https://github.com/ORM)\Column(type="geometry[]")
*/
private array $geometries;
/**
* [@var](https://github.com/var) WktSpatialData[]
* [@ORM](https://github.com/ORM)\Column(type="geography[]")
*/
private array $geographies;
public function setGeometries(WktSpatialData ...$geometries): void
{
$this->geometries = $geometries;
}
}
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\WktSpatialData;
// Single-item geometry[] array (supported)
$qb = $connection->createQueryBuilder();
$qb->insert('locations')->values(['geometries' => ':wktSpatialData']);
$qb->setParameter('wktSpatialData', [WktSpatialData::fromWkt('POINT(0 0)')], 'geometry[]');
$qb->executeStatement();
// Single geography value
$qb = $connection->createQueryBuilder();
$qb->insert('places')->values(['locations' => ':wktSpatialData']);
$qb->setParameter('wktSpatialData', WktSpatialData::fromWkt('SRID=4326;POINT(-122.4194 37.7749)'), 'geography');
$qb->executeStatement();
Note: Multi-item arrays have limitations — see "Important Limitation: Multi-Item Arrays" below.
// Single-item arrays
$singleGeometry = [WktSpatialData::fromWkt('POINT(0 0)')];
$singleGeography = [WktSpatialData::fromWkt('SRID=4326;POINT(-122.4194 37.7749)')];
// Complex single geometries
$complexGeometry = [WktSpatialData::fromWkt('POLYGON((0 0,0 1,1 1,1 0,0 0))')];
$geometryWithSrid = [WktSpatialData::fromWkt('SRID=4326;LINESTRING(-122 37,-121 38)')];
Multi-item geometry and geography arrays have a fundamental limitation with Doctrine DBAL parameter binding due to PostGIS parsing behavior.
When Doctrine DBAL tries to bind a multi-item array like:
$multiItem = [
WktSpatialData::fromWkt('POINT(1 2)'),
WktSpatialData::fromWkt('POINT(3 4)'),
];
It generates a PostgreSQL array literal: {POINT(1 2),POINT(3 4)}
However, PostGIS intercepts this and tries to parse the entire string as a single geometry, causing this error:
ERROR: parse error - invalid geometry
HINT: "POINT(1 2),POI" <-- parse error at position 14
This is a PostGIS-specific limitation, not a bug in our implementation:
$sql = "INSERT INTO locations (geometries) VALUES (ARRAY[?::geometry, ?::geometry])";
$connection->executeStatement($sql, ['POINT(1 2)', 'POINT(3 4)']);
// Instead of one multi-item array
$geometries = [$geom1, $geom2, $geom3];
// Use multiple single-item arrays
foreach ($geometries as $geometry) {
$entity->addGeometry([$geometry]);
}
The library normalizes dimensional modifiers based on enums for geometry types and modifiers.
Examples:
POINTZ(1 2 3) => POINT Z(1 2 3)
LINESTRINGM(0 0 1, 1 1 2) => LINESTRING M(0 0 1, 1 1 2)
POLYGONZM((...)) => POLYGON ZM((...))
POINT Z (1 2 3) => POINT Z(1 2 3)
SRID=4326;POINT Z (1 2 3) => SRID=4326;POINT Z(1 2 3)
See also: Spatial foundations and parser behavior in the Spatial Types document.
// Build arrays in application code, then use raw SQL with placeholders
$geometries = ['POINT(1 2)', 'POINT(3 4)', 'LINESTRING(0 0,1 1)'];
$placeholders = implode(',', array_fill(0, count($geometries), '?::geometry'));
$sql = "INSERT INTO locations (geometries) VALUES (ARRAY[$placeholders])";
$connection->executeStatement($sql, $geometries);
For complex multi-item scenarios, consider using JSON storage:
/**
* [@ORM](https://github.com/ORM)\Column(type="json")
*/
private array $geometriesAsJson;
public function setGeometries(array $wktStrings): void
{
$this->geometriesAsJson = $wktStrings;
}
public function getGeometries(): array
{
return array_map(
fn(string $wkt) => WktSpatialData::fromWkt($wkt),
$this->geometriesAsJson
);
}
The integration tests include both working single-item arrays and workarounded (through ARRAY[]) multi-item arrays to provide complete documentation of the PostGIS limitation.
SRID=4326;POINT(-122 37)geometry/geography and cannot directly index SQL array types like geometry[]. For proper spatial indexing, consider:
geometry column for GiST indexingThis limitation may be addressed in future versions through:
For now, the workarounds provide full functionality while maintaining type safety and spatial capabilities.
How can I help you explore Laravel packages today?