Skip to content

Commit 8ac7a70

Browse files
feat(#458): ensure Doctrine returns PostgreSQL spatial data in text format (EWKT) instead of binary format (EWKB) (#462)
1 parent 8176c22 commit 8ac7a70

File tree

7 files changed

+397
-2
lines changed

7 files changed

+397
-2
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\DBAL\Types;
6+
7+
use Doctrine\DBAL\Platforms\AbstractPlatform;
8+
9+
/**
10+
* Base class for PostGIS spatial types (GEOMETRY, GEOGRAPHY).
11+
*
12+
* Provides common functionality for spatial types that need to convert
13+
* between binary (EWKB) and text (EWKT) formats.
14+
*
15+
* @since 3.6
16+
*
17+
* @author Martin Georgiev <martin.georgiev@gmail.com>
18+
*/
19+
abstract class BaseSpatialType extends BaseType
20+
{
21+
/**
22+
* Modifies the SQL expression to convert spatial data from binary to EWKT format.
23+
*
24+
* This ensures PostgreSQL returns spatial data in text format (EWKT) instead of binary (EWKB).
25+
* This method is called by Doctrine ORM when generating SELECT queries.
26+
*
27+
* The SQL expression handles SRID:
28+
* - If SRID is 0 (no SRID), returns plain WKT: "POINT(1 2)"
29+
* - If SRID is set, returns EWKT with SRID prefix: "SRID=4326;POINT(1 2)"
30+
*
31+
* @param non-empty-string $sqlExpr
32+
* @param AbstractPlatform $platform
33+
*/
34+
public function convertToPHPValueSQL($sqlExpr, $platform): string
35+
{
36+
return \sprintf(
37+
"CASE WHEN ST_SRID(%s) = 0 THEN ST_AsText(%s) ELSE 'SRID=' || ST_SRID(%s) || ';' || ST_AsText(%s) END",
38+
$sqlExpr,
39+
$sqlExpr,
40+
$sqlExpr,
41+
$sqlExpr
42+
);
43+
}
44+
}

src/MartinGeorgiev/Doctrine/DBAL/Types/Geography.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*
1919
* @author Martin Georgiev <martin.georgiev@gmail.com>
2020
*/
21-
final class Geography extends BaseType
21+
final class Geography extends BaseSpatialType
2222
{
2323
protected const TYPE_NAME = 'geography';
2424

src/MartinGeorgiev/Doctrine/DBAL/Types/Geometry.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*
1919
* @author Martin Georgiev <martin.georgiev@gmail.com>
2020
*/
21-
final class Geometry extends BaseType
21+
final class Geometry extends BaseSpatialType
2222
{
2323
protected const TYPE_NAME = 'geometry';
2424

tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/GeographyTypeTest.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44

55
namespace Tests\Integration\MartinGeorgiev\Doctrine\DBAL\Types;
66

7+
use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries;
78
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\WktSpatialData;
89
use PHPUnit\Framework\Attributes\DataProvider;
910
use PHPUnit\Framework\Attributes\Test;
1011

1112
final class GeographyTypeTest extends TestCase
1213
{
14+
use OrmEntityPersistenceTrait;
15+
use WktAssertionTrait;
16+
1317
protected function getTypeName(): string
1418
{
1519
return 'geography';
@@ -20,6 +24,43 @@ protected function getPostgresTypeName(): string
2024
return 'GEOGRAPHY';
2125
}
2226

27+
protected function getEntityClass(): string
28+
{
29+
return ContainsGeometries::class;
30+
}
31+
32+
protected function getEntityColumnName(): string
33+
{
34+
return 'geography1';
35+
}
36+
37+
protected function createTestTableForEntity(string $tableName): void
38+
{
39+
$this->dropTestTableIfItExists($tableName);
40+
41+
$fullTableName = \sprintf('%s.%s', self::DATABASE_SCHEMA, $tableName);
42+
$sql = \sprintf('
43+
CREATE TABLE %s (
44+
id SERIAL PRIMARY KEY,
45+
geometry1 GEOMETRY,
46+
geometry2 GEOMETRY,
47+
geography1 GEOGRAPHY,
48+
geography2 GEOGRAPHY
49+
)
50+
', $fullTableName);
51+
52+
$this->connection->executeStatement($sql);
53+
}
54+
55+
protected function assertOrmValueEquals(mixed $expected, mixed $actual, string $typeName): void
56+
{
57+
if (!$expected instanceof WktSpatialData || !$actual instanceof WktSpatialData) {
58+
throw new \InvalidArgumentException('Expected WktSpatialData value objects.');
59+
}
60+
61+
$this->assertWktEquals($expected, $actual);
62+
}
63+
2364
protected function getSelectExpression(string $columnName): string
2465
{
2566
// For geography, avoid adding SRID prefix to preserve original input format
@@ -39,6 +80,26 @@ public function can_handle_geography_values(string $testName, WktSpatialData $wk
3980
$this->runDbalBindingRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), $wktSpatialData);
4081
}
4182

83+
#[Test]
84+
public function can_retrieve_null_geography_using_entity_manager_find(): void
85+
{
86+
$this->runOrmFindRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), null);
87+
}
88+
89+
#[DataProvider('provideValidTransformations')]
90+
#[Test]
91+
public function can_retrieve_geography_values_using_entity_manager_find(string $testName, WktSpatialData $wktSpatialData): void
92+
{
93+
$this->runOrmFindRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), $wktSpatialData);
94+
}
95+
96+
#[DataProvider('provideValidTransformations')]
97+
#[Test]
98+
public function can_retrieve_geography_values_using_dql_select(string $testName, WktSpatialData $wktSpatialData): void
99+
{
100+
$this->runOrmDqlSelectRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), $wktSpatialData);
101+
}
102+
42103
/**
43104
* @return array<string, array{string, WktSpatialData}>
44105
*/

tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/GeometryTypeTest.php

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44

55
namespace Tests\Integration\MartinGeorgiev\Doctrine\DBAL\Types;
66

7+
use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries;
78
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\WktSpatialData;
89
use PHPUnit\Framework\Attributes\DataProvider;
910
use PHPUnit\Framework\Attributes\Test;
1011

1112
final class GeometryTypeTest extends TestCase
1213
{
14+
use OrmEntityPersistenceTrait;
15+
use WktAssertionTrait;
16+
1317
protected function getTypeName(): string
1418
{
1519
return 'geometry';
@@ -20,6 +24,43 @@ protected function getPostgresTypeName(): string
2024
return 'GEOMETRY';
2125
}
2226

27+
protected function getEntityClass(): string
28+
{
29+
return ContainsGeometries::class;
30+
}
31+
32+
protected function getEntityColumnName(): string
33+
{
34+
return 'geometry1';
35+
}
36+
37+
protected function createTestTableForEntity(string $tableName): void
38+
{
39+
$this->dropTestTableIfItExists($tableName);
40+
41+
$fullTableName = \sprintf('%s.%s', self::DATABASE_SCHEMA, $tableName);
42+
$sql = \sprintf('
43+
CREATE TABLE %s (
44+
id SERIAL PRIMARY KEY,
45+
geometry1 GEOMETRY,
46+
geometry2 GEOMETRY,
47+
geography1 GEOGRAPHY,
48+
geography2 GEOGRAPHY
49+
)
50+
', $fullTableName);
51+
52+
$this->connection->executeStatement($sql);
53+
}
54+
55+
protected function assertOrmValueEquals(mixed $expected, mixed $actual, string $typeName): void
56+
{
57+
if (!$expected instanceof WktSpatialData || !$actual instanceof WktSpatialData) {
58+
throw new \InvalidArgumentException('Expected WktSpatialData value objects.');
59+
}
60+
61+
$this->assertWktEquals($expected, $actual);
62+
}
63+
2364
protected function getSelectExpression(string $columnName): string
2465
{
2566
return \sprintf(
@@ -46,6 +87,26 @@ public function can_handle_geometry_values(string $testName, WktSpatialData $wkt
4687
$this->runDbalBindingRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), $wktSpatialData);
4788
}
4889

90+
#[Test]
91+
public function can_retrieve_null_geometry_using_entity_manager_find(): void
92+
{
93+
$this->runOrmFindRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), null);
94+
}
95+
96+
#[DataProvider('provideValidTransformations')]
97+
#[Test]
98+
public function can_retrieve_geometry_values_using_entity_manager_find(string $testName, WktSpatialData $wktSpatialData): void
99+
{
100+
$this->runOrmFindRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), $wktSpatialData);
101+
}
102+
103+
#[DataProvider('provideValidTransformations')]
104+
#[Test]
105+
public function can_retrieve_geometry_values_using_dql_select(string $testName, WktSpatialData $wktSpatialData): void
106+
{
107+
$this->runOrmDqlSelectRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), $wktSpatialData);
108+
}
109+
49110
/**
50111
* @return array<string, array{string, WktSpatialData}>
51112
*/

0 commit comments

Comments
 (0)