Skip to content

Commit a33fcd8

Browse files
committed
Introduce Geometry value objects to eliminate leaky abstractions
Refactor GeometryType and GeographyType to replace direct WKT string handling with proper value objects. This prevents exposing database-specific string formats (WKT/EWKT) to application code. This commit introduces the Geometry value object, encapsulating WKT text and optional SRID metadata, and a supporting WKT value object for validating and wrapping Well-Known Text representations. Platform conversions were updated accordingly to handle WKT and EWKT formats transparently. Separating WKT and SRID metadata enables consistent spatial handling across databases (e.g., MySQL stores SRID in binary format, PostgreSQL uses EWKT text).
1 parent 4b50807 commit a33fcd8

File tree

10 files changed

+466
-16
lines changed

10 files changed

+466
-16
lines changed

src/Platforms/AbstractPlatform.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,36 @@ abstract public function getGeometryFromTextSQL(string $sqlExpr): string;
402402
*/
403403
abstract public function getGeometryAsTextSQL(string $sqlExpr): string;
404404

405+
/**
406+
* Converts a Geometry value object to the database representation.
407+
*
408+
* This method handles platform-specific conversion of Geometry objects (WKT + SRID)
409+
* to the format expected by the database.
410+
*
411+
* @param Types\Geometry $geometry The geometry value object to convert
412+
*
413+
* @return string The database-ready string representation (standard WKT)
414+
*/
415+
public function convertGeometryToDatabaseValue(Types\Geometry $geometry): string
416+
{
417+
return $geometry->getWkt()->toString();
418+
}
419+
420+
/**
421+
* Converts a database geometry value to a Geometry value object.
422+
*
423+
* This method handles platform-specific conversion from database format to Geometry objects.
424+
* The default implementation parses standard WKT without SRID.
425+
*
426+
* @param string $value The database value (standard WKT string)
427+
*
428+
* @return Types\Geometry The converted geometry value object
429+
*/
430+
public function convertGeometryFromDatabaseValue(string $value): Types\Geometry
431+
{
432+
return Types\Geometry::fromWkt($value);
433+
}
434+
405435
/**
406436
* Gets the SQL declaration snippet for a geography column.
407437
*

src/Platforms/PostgreSQLPlatform.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Doctrine\DBAL\Schema\Sequence;
1818
use Doctrine\DBAL\Schema\TableDiff;
1919
use Doctrine\DBAL\TransactionIsolationLevel;
20+
use Doctrine\DBAL\Types\Geometry;
2021
use Doctrine\DBAL\Types\Types;
2122
use Doctrine\Deprecations\Deprecation;
2223
use UnexpectedValueException;
@@ -826,7 +827,7 @@ public function getJsonbTypeDeclarationSQL(array $column): string
826827
{
827828
return 'JSONB';
828829
}
829-
830+
830831
/**
831832
* {@inheritDoc}
832833
*/
@@ -848,6 +849,26 @@ public function getGeometryAsTextSQL(string $sqlExpr): string
848849
return sprintf('ST_AsEWKT(%s)', $sqlExpr);
849850
}
850851

852+
/**
853+
* PostgreSQL/PostGIS uses EWKT format which includes the SRID prefix.
854+
*
855+
* {@inheritDoc}
856+
*/
857+
public function convertGeometryToDatabaseValue(Geometry $geometry): string
858+
{
859+
return $geometry->toEwkt();
860+
}
861+
862+
/**
863+
* PostgreSQL/PostGIS returns EWKT format which includes the SRID prefix.
864+
*
865+
* {@inheritDoc}
866+
*/
867+
public function convertGeometryFromDatabaseValue(string $value): Geometry
868+
{
869+
return Geometry::fromEwkt($value);
870+
}
871+
851872
/**
852873
* {@inheritDoc}
853874
*/

src/Types/GeographyType.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use function is_string;
1212

1313
/**
14-
* Type that maps a database GEOGRAPHY column to a PHP string containing WKT/EWKT data.
14+
* Type that maps a database GEOGRAPHY column to a PHP Geometry value object.
1515
*
1616
* Geography types handle spherical coordinate systems and are typically used for
1717
* earth-based geographic data with latitude/longitude coordinates.
@@ -38,21 +38,21 @@ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform)
3838
return null;
3939
}
4040

41-
if (is_string($value)) {
42-
return $value;
41+
if ($value instanceof Geometry) {
42+
return $value->toEwkt();
4343
}
4444

4545
throw ValueNotConvertible::new($value, Types::GEOGRAPHY);
4646
}
4747

48-
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?string
48+
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): Geometry|null
4949
{
5050
if ($value === null) {
5151
return null;
5252
}
5353

5454
if (is_string($value)) {
55-
return $value;
55+
return Geometry::fromEwkt($value);
5656
}
5757

5858
throw ValueNotConvertible::new($value, Types::GEOGRAPHY);

src/Types/Geometry.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Types;
6+
7+
use InvalidArgumentException;
8+
use Stringable;
9+
10+
use function preg_match;
11+
use function sprintf;
12+
13+
/**
14+
* Value object representing spatial geometry data with optional SRID (Spatial Reference System Identifier).
15+
*
16+
* This class separates the WKT representation from the SRID metadata, providing a clean abstraction
17+
* that works across different database platforms. PostgreSQL uses EWKT format (SRID prefix in text),
18+
* while MySQL stores SRID separately in the geometry binary format.
19+
*/
20+
final class Geometry implements Stringable
21+
{
22+
private function __construct(
23+
private readonly WKT $wkt,
24+
private readonly int|null $srid,
25+
) {
26+
}
27+
28+
/**
29+
* Creates a Geometry from WKT text and optional SRID.
30+
*
31+
* @param string $wkt Well-Known Text representation of the geometry
32+
* @param int|null $srid Spatial Reference System Identifier (e.g., 4326 for WGS84)
33+
*/
34+
public static function fromWkt(string $wkt, int|null $srid = null): self
35+
{
36+
return new self(WKT::fromString($wkt), $srid);
37+
}
38+
39+
/**
40+
* Creates a Geometry from Extended Well-Known Text (EWKT) format.
41+
*
42+
* EWKT format includes the SRID prefix: "SRID=4326;POINT(1 2)"
43+
* This method parses the SRID and WKT components automatically.
44+
*
45+
* @param string $ewkt Extended Well-Known Text with SRID prefix
46+
*
47+
* @throws InvalidArgumentException If the EWKT format is invalid.
48+
*/
49+
public static function fromEwkt(string $ewkt): self
50+
{
51+
if (preg_match('/^SRID=(\d+);(.+)$/is', $ewkt, $matches)) {
52+
return new self(
53+
WKT::fromString($matches[2]),
54+
(int) $matches[1],
55+
);
56+
}
57+
58+
// No SRID prefix, treat as standard WKT
59+
return new self(WKT::fromString($ewkt), null);
60+
}
61+
62+
/**
63+
* Returns the Well-Known Text representation.
64+
*/
65+
public function getWkt(): WKT
66+
{
67+
return $this->wkt;
68+
}
69+
70+
/**
71+
* Returns the Spatial Reference System Identifier, if set.
72+
*/
73+
public function getSrid(): int|null
74+
{
75+
return $this->srid;
76+
}
77+
78+
/**
79+
* Returns Extended Well-Known Text format (EWKT) if SRID is set, otherwise standard WKT.
80+
*
81+
* Examples:
82+
* - With SRID: "SRID=4326;POINT(1 2)"
83+
* - Without SRID: "POINT(1 2)"
84+
*/
85+
public function toEwkt(): string
86+
{
87+
if ($this->srid !== null) {
88+
return sprintf('SRID=%d;%s', $this->srid, $this->wkt->toString());
89+
}
90+
91+
return $this->wkt->toString();
92+
}
93+
94+
/**
95+
* Returns Extended Well-Known Text format (alias for toEwkt).
96+
*/
97+
public function __toString(): string
98+
{
99+
return $this->toEwkt();
100+
}
101+
}

src/Types/GeometryType.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
use function is_string;
1212

1313
/**
14-
* Type that maps a database GEOMETRY column to a PHP string containing WKT/EWKT data.
14+
* Type that maps a database GEOMETRY column to a PHP Geometry value object.
1515
*
1616
* This type handles geometric data stored in various database formats and converts
17-
* it to/from Well-Known Text (WKT) or Extended Well-Known Text (EWKT) format for PHP processing.
17+
* it to/from a Geometry value object that encapsulates WKT text and optional SRID metadata.
1818
*/
1919
class GeometryType extends Type
2020
{
@@ -37,21 +37,21 @@ public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform)
3737
return null;
3838
}
3939

40-
if (is_string($value)) {
41-
return $value;
40+
if ($value instanceof Geometry) {
41+
return $platform->convertGeometryToDatabaseValue($value);
4242
}
4343

4444
throw ValueNotConvertible::new($value, Types::GEOMETRY);
4545
}
4646

47-
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?string
47+
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): Geometry|null
4848
{
4949
if ($value === null) {
5050
return null;
5151
}
5252

5353
if (is_string($value)) {
54-
return $value;
54+
return $platform->convertGeometryFromDatabaseValue($value);
5555
}
5656

5757
throw ValueNotConvertible::new($value, Types::GEOMETRY);

src/Types/WKT.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Types;
6+
7+
use InvalidArgumentException;
8+
use Stringable;
9+
10+
use function preg_match;
11+
use function sprintf;
12+
use function trim;
13+
14+
/**
15+
* Lightweight value object representing Well-Known Text (WKT) spatial data.
16+
*
17+
* This class validates and wraps WKT strings to ensure they conform to the WKT specification.
18+
* It is used internally by the Geometry value object which combines WKT with optional SRID metadata.
19+
*
20+
* For application use, prefer the Geometry class which provides a complete spatial value object
21+
* with SRID support. This WKT class is primarily for internal validation and type safety.
22+
*
23+
* @see Geometry For the complete spatial value object with SRID support
24+
*/
25+
final class WKT implements Stringable
26+
{
27+
/**
28+
* Regular expression pattern for validating standard WKT format.
29+
*
30+
* Validates:
31+
* - Geometry type: POINT, LINESTRING, POLYGON, MULTIPOINT, MULTILINESTRING, MULTIPOLYGON, GEOMETRYCOLLECTION
32+
* - Optional Z/M/ZM modifiers
33+
* - Basic structure validation (parentheses presence)
34+
*/
35+
private const WKT_PATTERN =
36+
'/^(?:POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)'
37+
. '(?:\s+(?:Z|M|ZM))?\s*\(.+\)$/is';
38+
39+
private function __construct(private readonly string $value)
40+
{
41+
}
42+
43+
/**
44+
* Creates a WKT value object from a string.
45+
*
46+
* @param string $wkt Well-Known Text string (standard WKT without SRID prefix)
47+
*
48+
* @throws InvalidArgumentException If the WKT string format is invalid.
49+
*/
50+
public static function fromString(string $wkt): self
51+
{
52+
$trimmed = trim($wkt);
53+
54+
if ($trimmed === '') {
55+
throw new InvalidArgumentException('WKT string cannot be empty');
56+
}
57+
58+
if (preg_match(self::WKT_PATTERN, $trimmed) !== 1) {
59+
throw new InvalidArgumentException(sprintf(
60+
'Invalid WKT format: "%s". Expected valid Well-Known Text format.',
61+
$wkt,
62+
));
63+
}
64+
65+
return new self($trimmed);
66+
}
67+
68+
/**
69+
* Returns the WKT string representation.
70+
*/
71+
public function toString(): string
72+
{
73+
return $this->value;
74+
}
75+
76+
/** @return string The WKT string representation */
77+
public function __toString(): string
78+
{
79+
return $this->value;
80+
}
81+
}

tests/Types/GeographyTypeTest.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Doctrine\DBAL\Platforms\AbstractPlatform;
99
use Doctrine\DBAL\Types\ConversionException;
1010
use Doctrine\DBAL\Types\GeographyType;
11+
use Doctrine\DBAL\Types\Geometry;
1112
use PHPUnit\Framework\MockObject\MockObject;
1213
use PHPUnit\Framework\TestCase;
1314

@@ -47,8 +48,9 @@ public function testReturnsCorrectBindingType(): void
4748

4849
public function testConvertToPHPValue(): void
4950
{
50-
self::assertIsString($this->type->convertToPHPValue('POINT(-122.4194 37.7749)', $this->platform));
51-
self::assertIsString($this->type->convertToPHPValue('', $this->platform));
51+
$result = $this->type->convertToPHPValue('POINT(-122.4194 37.7749)', $this->platform);
52+
self::assertInstanceOf(Geometry::class, $result);
53+
self::assertSame('POINT(-122.4194 37.7749)', $result->getWkt()->toString());
5254
}
5355

5456
public function testNullConversion(): void
@@ -57,6 +59,13 @@ public function testNullConversion(): void
5759
self::assertNull($this->type->convertToDatabaseValue(null, $this->platform));
5860
}
5961

62+
public function testConvertToDatabaseValueWithGeometryObject(): void
63+
{
64+
$geometry = Geometry::fromWkt('POINT(-122.4194 37.7749)');
65+
$result = $this->type->convertToDatabaseValue($geometry, $this->platform);
66+
self::assertSame('POINT(-122.4194 37.7749)', $result);
67+
}
68+
6069
public function testConvertToPHPValueFailsOnInvalidValue(): void
6170
{
6271
$this->expectException(ConversionException::class);

0 commit comments

Comments
 (0)