A geometry or geography column maps to the WktSpatialData value object, which holds the value as WKT or EWKT text. This page shows how to build one, which geometry types it accepts, how to constrain a column, and what fails.
A dimensional modifier (Z, M or ZM) follows the type name, and the value you get back is always spelled POINT Z(…): one space before the modifier, none after it. What you write is not always what you read back:
You write
You read back
POINT Z (1 2 3)
POINT Z(1 2 3)
SRID=4326;POINT Z (1 2 3)
SRID=4326;POINT Z(1 2 3)
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((…))
WktSpatialData, the geometry and geography types and their arrays all read these the same way. Compare values in the spelling on the right, not in the string you started from.
Creating spatial data
Build a WktSpatialData from a string, from its parts, or with the point shortcuts:
useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\WktSpatialData;useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\GeometryType;useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\DimensionalModifier;$point=WktSpatialData::fromComponents(GeometryType::POINT,'1 2');// With SRID$pointWithSrid=WktSpatialData::fromComponents(GeometryType::POINT,'-122.4194 37.7749',4326);// With dimensional modifier$line3d=WktSpatialData::fromComponents(GeometryType::LINESTRING,'0 0 1, 1 1 2, 2 2 3',null,DimensionalModifier::Z);// With all parameters$polygon4d=WktSpatialData::fromComponents(GeometryType::POLYGON,'0 0 0 1, 0 1 0 1, 1 1 0 1, 1 0 0 1, 0 0 0 1',4326,DimensionalModifier::ZM);
Point shortcuts
useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\WktSpatialData;// Simple 2D point$point=WktSpatialData::point(1,2);// Result: POINT(1 2)// Point with SRID (common for geographic coordinates)$location=WktSpatialData::point(-122.4194,37.7749,4326);// Result: SRID=4326;POINT(-122.4194 37.7749)// 3D point with elevation$point3d=WktSpatialData::point3d(-122.4194,37.7749,100);// Result: POINT Z(-122.4194 37.7749 100)// 3D point with SRID$location3d=WktSpatialData::point3d(-122.4194,37.7749,100,4326);// Result: SRID=4326;POINT Z(-122.4194 37.7749 100)
Supported geometry types
WktSpatialData accepts every geometry type PostGIS has; the GeometryType enum lists them.
By default geometry and geography columns, and their arrays, are declared as bare GEOMETRY / GEOGRAPHY. A bare GEOMETRY column accepts any subtype and any SRID. A bare GEOGRAPHY column is narrower: it accepts only geography-compatible subtypes (PostGIS rejects TIN, for example) and geodetic lon/lat SRIDs, and stores SRID-less input as SRID 4326. Two column options add a PostGIS type modifier so the constraint is enforced by PostgreSQL itself:
Option
Type
Meaning
geometry_type
string
The geometry subtype, such as Point, LineString, MultiPolygon. Case-insensitive. May carry a dimensional modifier suffix: PointZ, PointM, PointZM. Use Geometry for “any subtype”.
srid
int
The spatial reference system identifier, such as 4326. Must be a non-negative integer.
useDoctrine\ORM\MappingasORM;useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\WktSpatialData;#[ORM\Entity]classPlace{// GEOGRAPHY(POINT,4326)#[ORM\Column(type: 'geography', options: ['geometry_type' => 'Point', 'srid' => 4326])]privateWktSpatialData$location;// GEOMETRY(POLYGONZ) - 3D polygons, SRID unconstrained#[ORM\Column(type: 'geometry', options: ['geometry_type' => 'PolygonZ'])]privateWktSpatialData$volume;// GEOMETRY(GEOMETRY,3857) - any subtype, but SRID pinned to Web Mercator#[ORM\Column(type: 'geometry', options: ['srid' => 3857])]privateWktSpatialData$tileShape;// GEOMETRY - unconstrained#[ORM\Column(type: 'geometry')]privateWktSpatialData$shape;// GEOGRAPHY(POINT,4326)[] - every item a WGS 84 point/** @var list<?WktSpatialData> */#[ORM\Column(type: 'geography[]', options: ['geometry_type' => 'Point', 'srid' => 4326])]privatearray$stops;}
An array type carries the modifier on its item type, GEOGRAPHY(POINT,4326)[], and PostgreSQL checks every item against it.
Both options are optional and independent:
Neither option → bare GEOMETRY / GEOGRAPHY.
geometry_type only → GEOMETRY(POINT).
srid only → GEOMETRY(GEOMETRY,4326), since PostGIS requires a subtype whenever an SRID is given.
Caveats
The options only shape the DDL that Doctrine generates. They do not alter value conversion - a WktSpatialData carrying a different subtype is still handed to PostgreSQL, which rejects it at insert time.
geography only supports lon/lat reference systems; PostgreSQL rejects GEOGRAPHY(POINT,3857) at CREATE TABLE time.
A constrained column coerces values that carry no SRID: inserting POINT(1 2) into GEOMETRY(POINT,4326) stores SRID=4326;POINT(1 2).
Doctrine’s schema comparator does not understand PostGIS type modifiers, so doctrine:schema:update and diff-based migration generation may report spurious changes for these columns. Manage them with explicit migrations.
Spatial (GiST) indexes are not covered by these options. Declare them in a migration with raw SQL: CREATE INDEX idx_place_location ON place USING GIST (location);
Geography vs geometry specifics
Both accept WKT and EWKT, which adds the SRID in front: SRID=4326;POINT(1 2).
A geography value always has an SRID. PostGIS stores SRID-less input as 4326, so POINT(1 2) reads back as SRID=4326;POINT(1 2).
Both normalize the dimensional modifiers (Z, M, ZM) the same way.
Arrays
GEOMETRY[] and GEOGRAPHY[] bind through DBAL parameter binding, with any number of elements.
A null element is written as a SQL NULL element and read back as null.
A malformed string fails when you build the value object, before anything reaches PostgreSQL. A wrong value handed to the DBAL type fails in the type.
Invalid WKT
useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Exceptions\InvalidWktSpatialDataException;useMartinGeorgiev\Doctrine\DBAL\Types\ValueObject\WktSpatialData;try{// Unknown geometry type$invalid=WktSpatialData::fromString('INVALID(1 2)');}catch(InvalidWktSpatialDataException$e){// Throws: "Unsupported geometry type: 'INVALID'. Supported types: POINT, LINESTRING, POLYGON, …"}try{// Empty coordinate section$empty=WktSpatialData::fromString('POINT()');}catch(InvalidWktSpatialDataException$e){// Throws: "Invalid Wkt: empty coordinate/body section"}try{// Invalid SRID format$invalidSrid=WktSpatialData::fromString('SRID=abc;POINT(1 2)');}catch(InvalidWktSpatialDataException$e){// Throws: "Invalid Srid value in Ewkt: 'abc'"}try{// Missing semicolon in EWKT$missingSemicolon=WktSpatialData::fromString('SRID=4326POINT(1 2)');}catch(InvalidWktSpatialDataException$e){// Throws: "Invalid Ewkt: missing semicolon after Srid prefix"}
Invalid values in the DBAL type
useMartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidGeometryForDatabaseException;useMartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidGeometryForPHPException;// Invalid type passed to a geometry columntry{$geometryType->convertToDatabaseValue('not a geometry',$platform);}catch(InvalidGeometryForDatabaseException$e){// Throws: "Value must be a Geometry value object, 'not a geometry' given"}// Invalid format from the databasetry{$geometryType->convertToPHPValue('invalid wkt from db',$platform);}catch(InvalidGeometryForPHPException$e){// Throws: "Invalid Geometry value object format: 'invalid wkt from db'"}
Checking a value before you use it
fromString() is the validator: catch its exception to test a string, then read the type and SRID from the value object.
functionvalidateSpatialData(string$wkt):bool{try{WktSpatialData::fromString($wkt);returntrue;}catch(InvalidWktSpatialDataException){returnfalse;}}$spatialData=WktSpatialData::fromString('SRID=4326;POINT(-122 37)');$spatialData->getGeometryType();// GeometryType::POINT$spatialData->getSrid();// 4326, or null when the string carries no SRID
How the values are parsed
WktSpatialData::fromString() reads WKT and EWKT. The type names and modifiers it accepts come from two enums, GeometryType and DimensionalModifier.
GeometryArray and GeographyArray share the SpatialDataArray base. It splits a PostgreSQL array literal into items, quoted or not, and keeps an item whole when its coordinate list contains commas or nested parentheses. Each item then goes through WktSpatialData::fromString(), the same parser a single value uses.