Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/MartinGeorgiev/Doctrine/DBAL/Types/BaseSpatialType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
* Base class for PostGIS spatial types (GEOMETRY, GEOGRAPHY).
*
* Provides common functionality for spatial types that need to convert
* between binary (EWKB) and text (EWKT) formats.
*
* @since 3.6
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
abstract class BaseSpatialType extends BaseType
{
/**
* Modifies the SQL expression to convert spatial data from binary to EWKT format.
*
* This ensures PostgreSQL returns spatial data in text format (EWKT) instead of binary (EWKB).
* This method is called by Doctrine ORM when generating SELECT queries.
*
* The SQL expression handles SRID:
* - If SRID is 0 (no SRID), returns plain WKT: "POINT(1 2)"
* - If SRID is set, returns EWKT with SRID prefix: "SRID=4326;POINT(1 2)"
*
* @param non-empty-string $sqlExpr
* @param AbstractPlatform $platform
*/
public function convertToPHPValueSQL($sqlExpr, $platform): string
{
return \sprintf(
"CASE WHEN ST_SRID(%s) = 0 THEN ST_AsText(%s) ELSE 'SRID=' || ST_SRID(%s) || ';' || ST_AsText(%s) END",
$sqlExpr,
$sqlExpr,
$sqlExpr,
$sqlExpr
);
}
}
2 changes: 1 addition & 1 deletion src/MartinGeorgiev/Doctrine/DBAL/Types/Geography.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
final class Geography extends BaseType
final class Geography extends BaseSpatialType
{
protected const TYPE_NAME = 'geography';

Expand Down
2 changes: 1 addition & 1 deletion src/MartinGeorgiev/Doctrine/DBAL/Types/Geometry.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
final class Geometry extends BaseType
final class Geometry extends BaseSpatialType
{
protected const TYPE_NAME = 'geometry';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@

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

use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries;
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\WktSpatialData;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;

final class GeographyTypeTest extends TestCase
{
use OrmEntityPersistenceTrait;
use WktAssertionTrait;

protected function getTypeName(): string
{
return 'geography';
Expand All @@ -20,6 +24,43 @@ protected function getPostgresTypeName(): string
return 'GEOGRAPHY';
}

protected function getEntityClass(): string
{
return ContainsGeometries::class;
}

protected function getEntityColumnName(): string
{
return 'geography1';
}

protected function createTestTableForEntity(string $tableName): void
{
$this->dropTestTableIfItExists($tableName);

$fullTableName = \sprintf('%s.%s', self::DATABASE_SCHEMA, $tableName);
$sql = \sprintf('
CREATE TABLE %s (
id SERIAL PRIMARY KEY,
geometry1 GEOMETRY,
geometry2 GEOMETRY,
geography1 GEOGRAPHY,
geography2 GEOGRAPHY
)
', $fullTableName);

$this->connection->executeStatement($sql);
}

protected function assertOrmValueEquals(mixed $expected, mixed $actual, string $typeName): void
{
if (!$expected instanceof WktSpatialData || !$actual instanceof WktSpatialData) {
throw new \InvalidArgumentException('Expected WktSpatialData value objects.');
}

$this->assertWktEquals($expected, $actual);
}

protected function getSelectExpression(string $columnName): string
{
// For geography, avoid adding SRID prefix to preserve original input format
Expand All @@ -39,6 +80,26 @@ public function can_handle_geography_values(string $testName, WktSpatialData $wk
$this->runDbalBindingRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), $wktSpatialData);
}

#[Test]
public function can_retrieve_null_geography_using_entity_manager_find(): void
{
$this->runOrmFindRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), null);
}

#[DataProvider('provideValidTransformations')]
#[Test]
public function can_retrieve_geography_values_using_entity_manager_find(string $testName, WktSpatialData $wktSpatialData): void
{
$this->runOrmFindRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), $wktSpatialData);
}

#[DataProvider('provideValidTransformations')]
#[Test]
public function can_retrieve_geography_values_using_dql_select(string $testName, WktSpatialData $wktSpatialData): void
{
$this->runOrmDqlSelectRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), $wktSpatialData);
}

/**
* @return array<string, array{string, WktSpatialData}>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@

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

use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsGeometries;
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\WktSpatialData;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;

final class GeometryTypeTest extends TestCase
{
use OrmEntityPersistenceTrait;
use WktAssertionTrait;

protected function getTypeName(): string
{
return 'geometry';
Expand All @@ -20,6 +24,43 @@ protected function getPostgresTypeName(): string
return 'GEOMETRY';
}

protected function getEntityClass(): string
{
return ContainsGeometries::class;
}

protected function getEntityColumnName(): string
{
return 'geometry1';
}

protected function createTestTableForEntity(string $tableName): void
{
$this->dropTestTableIfItExists($tableName);

$fullTableName = \sprintf('%s.%s', self::DATABASE_SCHEMA, $tableName);
$sql = \sprintf('
CREATE TABLE %s (
id SERIAL PRIMARY KEY,
geometry1 GEOMETRY,
geometry2 GEOMETRY,
geography1 GEOGRAPHY,
geography2 GEOGRAPHY
)
', $fullTableName);

$this->connection->executeStatement($sql);
}

protected function assertOrmValueEquals(mixed $expected, mixed $actual, string $typeName): void
{
if (!$expected instanceof WktSpatialData || !$actual instanceof WktSpatialData) {
throw new \InvalidArgumentException('Expected WktSpatialData value objects.');
}

$this->assertWktEquals($expected, $actual);
}

protected function getSelectExpression(string $columnName): string
{
return \sprintf(
Expand All @@ -46,6 +87,26 @@ public function can_handle_geometry_values(string $testName, WktSpatialData $wkt
$this->runDbalBindingRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), $wktSpatialData);
}

#[Test]
public function can_retrieve_null_geometry_using_entity_manager_find(): void
{
$this->runOrmFindRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), null);
}

#[DataProvider('provideValidTransformations')]
#[Test]
public function can_retrieve_geometry_values_using_entity_manager_find(string $testName, WktSpatialData $wktSpatialData): void
{
$this->runOrmFindRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), $wktSpatialData);
}

#[DataProvider('provideValidTransformations')]
#[Test]
public function can_retrieve_geometry_values_using_dql_select(string $testName, WktSpatialData $wktSpatialData): void
{
$this->runOrmDqlSelectRoundTrip($this->getTypeName(), $this->getPostgresTypeName(), $wktSpatialData);
}

/**
* @return array<string, array{string, WktSpatialData}>
*/
Expand Down
Loading
Loading