postgresql-for-doctrine
postgresql-for-doctrine copied to clipboard
Support PostGIS
Hi, It would be nice to have PostGIS support :).
Sounds exciting. Please help with a PR proposing the implementation.
@seb-jean do you know about https://github.com/jsor/doctrine-postgis?
@seb-jean do you know about https://github.com/jsor/doctrine-postgis?
It is unfortunately abandoned.
It is unfortunately abandoned.
Abandoned? It's not particularly alive, but why would it need to be alive if it reached the state where doesn't need much? Last PR 7 months ago is far from abandoned.
Yes, I agree if there are no more developments to be done :). But, it's relative to the version of Doctrine used. PRs have been added without answers.
@martin-georgiev Do you agree to me adding PostGIS features to your repository?
Go ahead βΊοΈ
You can use @coderabbitai to help you with suggestions for the test coverage.
[!TIP] For best results, initiate chat on the files or code changes.
π§© Analysis chain
@martin-georgiev Thank you for the opportunity to help with test coverage suggestions for the PostGIS features! Let me first examine the repository structure and the specific requirements to provide comprehensive testing recommendations.
π Script executed:
#!/bin/bash
# First, let's understand the repository structure and existing tests
echo "=== Repository Structure ==="
fd -t f -e php | head -20
echo -e "\n=== Test Directory Structure ==="
fd -t f -e php tests/
echo -e "\n=== Current PostgreSQL Types ==="
fd -t f -e php src/ | xargs grep -l "class.*Type" | head -10
echo -e "\n=== Existing Test Patterns ==="
ls -la tests/
Length of output: 2397
π Script executed:
#!/bin/bash
# Let's examine the issue #305 content using GitHub CLI
gh issue view 305 --repo martin-georgiev/postgresql-for-doctrine
Length of output: 253
π Script executed:
#!/bin/bash
# Let's look at existing type implementations and their tests
echo "=== Existing Type Classes ==="
find src/ -name "*Type.php" | head -10
echo -e "\n=== Sample Type Implementation ==="
find src/ -name "*Type.php" | head -1 | xargs cat
echo -e "\n=== Sample Test Implementation ==="
find tests/ -name "*Test.php" | head -1 | xargs cat
Length of output: 8366
π Script executed:
#!/bin/bash
# Let's examine the test structure more closely
echo "=== Test File Naming Patterns ==="
find tests/ -name "*.php" | sort
echo -e "\n=== PHPUnit Configuration ==="
cat phpunit.xml* 2>/dev/null || cat phpunit.dist.xml 2>/dev/null || echo "No PHPUnit config found"
echo -e "\n=== Composer Dependencies (for testing frameworks) ==="
jq '.["require-dev"]' composer.json 2>/dev/null || echo "No composer.json found"
Length of output: 19180
Based on my analysis of the repository structure and existing patterns, I can now provide comprehensive test coverage recommendations for PostGIS features. Here are my suggestions:
Test Coverage Strategy for PostGIS Implementation
1. Core Spatial Types to Implement & Test
The most essential PostGIS types to start with:
// Geometry types
- geometry (base type)
- point (already exists, but may need PostGIS enhancement)
- linestring
- polygon
- multipoint
- multilinestring
- multipolygon
- geometrycollection
// Geography types
- geography
- geography(point)
- geography(linestring)
- geography(polygon)
2. Test Structure Recommendations
Following the existing patterns in your codebase:
Integration Tests (tests/Integration/MartinGeorgiev/Doctrine/DBAL/Types/)
Add PostGIS test cases to the existing DBALTypesTest.php:
#[DataProvider('providePostGISTypeTestCases')]
public function test_postgis_type(string $typeName, string $columnType, mixed $testValue): void
{
$this->runTypeTest($typeName, $columnType, $testValue);
}
public static function providePostGISTypeTestCases(): array
{
return [
// Geometry types
'geometry point' => ['geometry', 'GEOMETRY', 'POINT(1.23 4.56)'],
'geometry linestring' => ['geometry', 'GEOMETRY', 'LINESTRING(0 0, 1 1, 2 2)'],
'geometry polygon' => ['geometry', 'GEOMETRY', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))'],
'geometry multipoint' => ['geometry', 'GEOMETRY', 'MULTIPOINT((0 0), (1 1))'],
// Geography types
'geography point' => ['geography', 'GEOGRAPHY', 'POINT(-122.4194 37.7749)'],
'geography linestring' => ['geography', 'GEOGRAPHY', 'LINESTRING(-122.4194 37.7749, -122.4094 37.7849)'],
// Array types
'geometry[]' => ['geometry[]', 'GEOMETRY[]', ['POINT(0 0)', 'POINT(1 1)', 'LINESTRING(0 0, 1 1)']],
'geography[]' => ['geography[]', 'GEOGRAPHY[]', ['POINT(-122.4194 37.7749)', 'POINT(-122.4094 37.7849)']],
];
}
Unit Tests (tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/)
Create individual test files for each PostGIS type:
// GeometryTest.php
class GeometryTest extends TestCase
{
public function test_conversion_from_database_value(): void
public function test_conversion_to_database_value(): void
public function test_sql_declaration(): void
public function test_invalid_values_throw_exceptions(): void
}
// GeographyTest.php
class GeographyTest extends TestCase
{
// Similar structure
}
3. Comprehensive Test Cases to Include
Boundary Value Testing:
// Valid coordinate ranges
'geometry point valid coordinates' => ['geometry', 'GEOMETRY', 'POINT(-180 -90)'],
'geometry point boundary coordinates' => ['geometry', 'GEOMETRY', 'POINT(180 90)'],
'geography point antimeridian' => ['geography', 'GEOGRAPHY', 'POINT(180 0)'],
'geography point poles' => ['geography', 'GEOGRAPHY', 'POINT(0 90)'],
Complex Geometry Testing:
// Multi-geometries
'geometry collection mixed' => ['geometry', 'GEOMETRY', 'GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(0 0, 1 1))'],
'polygon with holes' => ['geometry', 'GEOMETRY', 'POLYGON((0 0, 0 10, 10 10, 10 0, 0 0), (2 2, 2 8, 8 8, 8 2, 2 2))'],
// Different coordinate dimensions
'geometry point 3d' => ['geometry', 'GEOMETRY', 'POINT Z (1 2 3)'],
'geometry point 4d' => ['geometry', 'GEOMETRY', 'POINT ZM (1 2 3 4)'],
SRID (Spatial Reference System) Testing:
'geometry with srid' => ['geometry', 'GEOMETRY', 'SRID=4326;POINT(-122.4194 37.7749)'],
'geography explicit srid' => ['geography', 'GEOGRAPHY', 'SRID=4326;POINT(-122.4194 37.7749)'],
Error Handling Tests:
public function test_invalid_wkt_throws_exception(): void
{
$this->expectException(ConversionException::class);
$type = Type::getType('geometry');
$type->convertToPHPValue('INVALID_WKT', $this->platform);
}
public function test_invalid_coordinates_throw_exception(): void
{
$this->expectException(ConversionException::class);
$type = Type::getType('geography');
$type->convertToPHPValue('POINT(200 100)', $this->platform); // Invalid longitude/latitude
}
4. Value Object Testing
Create PostGIS-specific value objects and test them thoroughly:
// GeometryValueObject tests
class GeometryValueObjectTest extends TestCase
{
public function test_wkt_parsing(): void
public function test_coordinate_extraction(): void
public function test_srid_handling(): void
public function test_geometry_type_detection(): void
public function test_equality_comparison(): void
public function test_serialization(): void
}
5. Performance & Scale Testing
Include tests for larger geometries:
public function test_large_polygon_performance(): void
{
// Test with polygon containing many vertices
$coordinates = [];
for ($i = 0; $i < 1000; $i++) {
$coordinates[] = $i . ' ' . ($i * 2);
}
$wkt = 'POLYGON((' . implode(', ', $coordinates) . ', 0 0))';
$this->runTypeTest('geometry', 'GEOMETRY', $wkt);
}
6. Database Function Integration Tests
Test PostGIS spatial functions (if you plan to add them):
// In Functions directory
class STAreaTest extends TestCase
class STDistanceTest extends TestCase
class STIntersectsTest extends TestCase
class STContainsTest extends TestCase
7. Fixture Entities for Testing
Create test entities in fixtures/ directory:
class ContainsGeometries extends Entity
{
private ?string $location = null; // geometry type
private ?string $boundary = null; // geography type
private ?array $waypoints = null; // geometry[] type
}
Would you like me to help create any specific test files or dive deeper into any particular aspect of the PostGIS testing strategy?
π Script executed:
#!/bin/bash
# Let's look at existing geometric/spatial types in the codebase
echo "=== Existing Spatial Types ==="
find src/ -name "*.php" | xargs grep -l -i "point\|geo\|spatial" | head -10
echo -e "\n=== Point Type Implementation ==="
find src/ -name "*Point*" -type f | xargs cat
echo -e "\n=== Point Value Object ==="
find src/ -name "*Point*" -type f | grep -v Type | xargs cat
Length of output: 9415
π Script executed:
#!/bin/bash
# Let's examine the existing Point tests to understand the testing patterns
echo "=== Point Integration Tests ==="
find tests/Integration/ -name "*Point*" -type f | xargs cat | head -50
echo -e "\n=== Point Unit Tests ==="
find tests/Unit/ -name "*Point*" -type f | xargs cat | head -50
Length of output: 1793
π Web query:
What are the most commonly used PostGIS spatial data types in PostgreSQL databases?
π‘ Result:
PostGIS extends PostgreSQL by adding specialized data types for storing and manipulating spatial data. The most commonly used PostGIS spatial data types are:
Primary Spatial Data Types
Geometry and Geography are the two primary spatial data types in PostGIS:
-
Geometry: This flexible type models shapes in the 2-dimensional Cartesian coordinate plane[1][7]. Each geometry value is associated with a spatial reference system (SRS) that defines the coordinate system and units of measurement[7]. This type is ideal for operations that don't need to account for Earth's curvature.
-
Geography: Specifically designed for large-scale spatial operations on the Earth's surface, this type factors in the Earth's curvature[7][8]. The coordinates for geography objects are specified in degrees of longitude and latitude, and all calculations return results in meters[8].
Geometry Subtypes
The geometry data type has several concrete subtypes representing various kinds and dimensions of geometric shapes:
Atomic Types
- Point: Represents a single location on Earth with a coordinate[2]
- LineString: A sequence of points forming a line
- LinearRing: A closed line that forms a ring
- Polygon: A closed area defined by an outer ring and optional inner rings[1]
Collection Types
- MultiPoint: A collection of points
- MultiLineString: A collection of line strings
- MultiPolygon: A collection of polygons
- GeometryCollection: A collection of geometries of different types[1]
Additional subtypes include PolyhedralSurface, Triangle, and TIN as specified in the Simple Features Access standard[1].
Working with Spatial Types
PostGIS implements the Open Geospatial Consortium (OGC) Geometry model, allowing users to store, index, and query geospatial data[5][9]. The spatial data types enable powerful operations like distance calculations, intersections, and spatial analysis through functions like ST_Distance, ST_DWithin, and many others[7].
Users can convert between geometry and geography types when needed using casting (::). For example, you can cast a geography object to geometry to access functions that only support the geometry type: geog::geometry[8].
These spatial data types, combined with PostGIS's spatial indexing capabilities (primarily R-Tree and Quadtree)[9] and extensive function library, transform PostgreSQL into a powerful spatial database for GIS applications.
Citations:
- 1: https://postgis.net/docs/using_postgis_dbmanagement.html
- 2: http://postgis.net/workshops/postgis-intro/geometries.html
- 3: https://www.sqlshack.com/getting-started-with-spatial-data-in-postgresql/
- 4: https://www.postgresql.org/docs/current/datatype-geometric.html
- 5: https://postgis.net
- 6: https://livebook.manning.com/book/postgis-in-action-second-edition/chapter-2
- 7: https://neon.tech/docs/extensions/postgis
- 8: http://postgis.net/workshops/postgis-intro/geography.html
- 9: https://postgis.net/workshops/postgis-intro/introduction.html
- 10: https://postgis.net/docs/reference.html
Perfect! Now I have a complete understanding of your existing patterns. The Point type implementation is already excellent and provides a perfect foundation. Let me provide enhanced test coverage recommendations that build on your existing architecture:
Enhanced PostGIS Test Coverage Strategy
1. Expand Integration Tests (Build on existing DBALTypesTest.php)
// Add to existing DBALTypesTest.php
#[DataProvider('providePostGISGeometryTypeTestCases')]
public function test_postgis_geometry_type(string $typeName, string $columnType, mixed $testValue): void
{
$this->runTypeTest($typeName, $columnType, $testValue);
}
public static function providePostGISGeometryTypeTestCases(): array
{
return [
// Basic geometry types using WKT (Well-Known Text)
'geometry point' => ['geometry', 'GEOMETRY', 'POINT(1.23 4.56)'],
'geometry linestring' => ['geometry', 'GEOMETRY', 'LINESTRING(0 0, 1 1, 2 2)'],
'geometry polygon simple' => ['geometry', 'GEOMETRY', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))'],
'geometry polygon with hole' => ['geometry', 'GEOMETRY', 'POLYGON((0 0, 0 10, 10 10, 10 0, 0 0), (2 2, 2 8, 8 8, 8 2, 2 2))'],
// Multi-geometries
'geometry multipoint' => ['geometry', 'GEOMETRY', 'MULTIPOINT((0 0), (1 1), (2 2))'],
'geometry multilinestring' => ['geometry', 'GEOMETRY', 'MULTILINESTRING((0 0, 1 1), (2 2, 3 3))'],
'geometry multipolygon' => ['geometry', 'GEOMETRY', 'MULTIPOLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)), ((2 2, 2 3, 3 3, 3 2, 2 2)))'],
// Geometry collections
'geometry collection mixed' => ['geometry', 'GEOMETRY', 'GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(0 0, 1 1), POLYGON((2 2, 2 3, 3 3, 3 2, 2 2)))'],
// 3D and 4D geometries
'geometry point 3d' => ['geometry', 'GEOMETRY', 'POINT Z (1 2 3)'],
'geometry point 4d' => ['geometry', 'GEOMETRY', 'POINT ZM (1 2 3 4)'],
'geometry linestring 3d' => ['geometry', 'GEOMETRY', 'LINESTRING Z (0 0 0, 1 1 1, 2 2 2)'],
// With SRID
'geometry point with srid' => ['geometry', 'GEOMETRY', 'SRID=4326;POINT(-122.4194 37.7749)'],
'geometry polygon with srid' => ['geometry', 'GEOMETRY', 'SRID=3857;POLYGON((0 0, 0 1000000, 1000000 1000000, 1000000 0, 0 0))'],
];
}
#[DataProvider('providePostGISGeographyTypeTestCases')]
public function test_postgis_geography_type(string $typeName, string $columnType, mixed $testValue): void
{
$this->runTypeTest($typeName, $columnType, $testValue);
}
public static function providePostGISGeographyTypeTestCases(): array
{
return [
// Geography types (always in WGS84/4326)
'geography point' => ['geography', 'GEOGRAPHY', 'POINT(-122.4194 37.7749)'],
'geography linestring' => ['geography', 'GEOGRAPHY', 'LINESTRING(-122.4194 37.7749, -122.4094 37.7849)'],
'geography polygon' => ['geography', 'GEOGRAPHY', 'POLYGON((-122.5 37.7, -122.5 37.8, -122.4 37.8, -122.4 37.7, -122.5 37.7))'],
// Constrained geography types
'geography(point)' => ['geography(point)', 'GEOGRAPHY(POINT)', 'POINT(-122.4194 37.7749)'],
'geography(linestring)' => ['geography(linestring)', 'GEOGRAPHY(LINESTRING)', 'LINESTRING(-122.4194 37.7749, -122.4094 37.7849)'],
'geography(polygon)' => ['geography(polygon)', 'GEOGRAPHY(POLYGON)', 'POLYGON((-122.5 37.7, -122.5 37.8, -122.4 37.8, -122.4 37.7, -122.5 37.7))'],
// Geography with SRID explicitly
'geography point with srid' => ['geography', 'GEOGRAPHY', 'SRID=4326;POINT(-122.4194 37.7749)'],
// Boundary cases for geography
'geography point antimeridian' => ['geography', 'GEOGRAPHY', 'POINT(180 0)'],
'geography point north pole' => ['geography', 'GEOGRAPHY', 'POINT(0 90)'],
'geography point south pole' => ['geography', 'GEOGRAPHY', 'POINT(0 -90)'],
];
}
#[DataProvider('providePostGISArrayTypeTestCases')]
public function test_postgis_array_type(string $typeName, string $columnType, array $testValue): void
{
$this->runTypeTest($typeName, $columnType, $testValue);
}
public static function providePostGISArrayTypeTestCases(): array
{
return [
'geometry[]' => ['geometry[]', 'GEOMETRY[]', [
'POINT(0 0)',
'LINESTRING(0 0, 1 1)',
'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))'
]],
'geography[]' => ['geography[]', 'GEOGRAPHY[]', [
'POINT(-122.4194 37.7749)',
'LINESTRING(-122.4194 37.7749, -122.4094 37.7849)',
'POLYGON((-122.5 37.7, -122.5 37.8, -122.4 37.8, -122.4 37.7, -122.5 37.7))'
]],
];
}
2. Specialized Unit Tests (Following your existing pattern)
Create individual test files for each PostGIS type:
GeometryTest.php
<?php
declare(strict_types=1);
namespace Tests\Unit\MartinGeorgiev\Doctrine\DBAL\Types;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use MartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidGeometryForDatabaseException;
use MartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidGeometryForPHPException;
use MartinGeorgiev\Doctrine\DBAL\Types\Geometry;
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Geometry as GeometryValueObject;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
class GeometryTest extends TestCase
{
private MockObject $platform;
private Geometry $fixture;
protected function setUp(): void
{
$this->platform = $this->createMock(AbstractPlatform::class);
$this->fixture = new Geometry();
}
#[Test]
public function has_name(): void
{
self::assertEquals('geometry', $this->fixture->getName());
}
#[DataProvider('provideValidWKTStrings')]
#[Test]
public function can_convert_from_database_value(string $wkt, GeometryValueObject $expected): void
{
$result = $this->fixture->convertToPHPValue($wkt, $this->platform);
self::assertInstanceOf(GeometryValueObject::class, $result);
self::assertEquals($expected->getWKT(), $result->getWKT());
self::assertEquals($expected->getSRID(), $result->getSRID());
}
#[DataProvider('provideValidWKTStrings')]
#[Test]
public function can_convert_to_database_value(string $expectedWkt, GeometryValueObject $geometry): void
{
$result = $this->fixture->convertToDatabaseValue($geometry, $this->platform);
self::assertEquals($expectedWkt, $result);
}
#[Test]
public function returns_null_for_null_database_value(): void
{
self::assertNull($this->fixture->convertToPHPValue(null, $this->platform));
}
#[Test]
public function returns_null_for_null_php_value(): void
{
self::assertNull($this->fixture->convertToDatabaseValue(null, $this->platform));
}
#[DataProvider('provideInvalidDatabaseValues')]
#[Test]
public function throws_exception_for_invalid_database_value(mixed $invalidValue): void
{
$this->expectException(InvalidGeometryForDatabaseException::class);
$this->fixture->convertToPHPValue($invalidValue, $this->platform);
}
#[DataProvider('provideInvalidPHPValues')]
#[Test]
public function throws_exception_for_invalid_php_value(mixed $invalidValue): void
{
$this->expectException(InvalidGeometryForPHPException::class);
$this->fixture->convertToDatabaseValue($invalidValue, $this->platform);
}
public static function provideValidWKTStrings(): array
{
return [
'point' => [
'POINT(1.23 4.56)',
GeometryValueObject::fromWKT('POINT(1.23 4.56)')
],
'point with srid' => [
'SRID=4326;POINT(-122.4194 37.7749)',
GeometryValueObject::fromWKT('SRID=4326;POINT(-122.4194 37.7749)')
],
'linestring' => [
'LINESTRING(0 0, 1 1, 2 2)',
GeometryValueObject::fromWKT('LINESTRING(0 0, 1 1, 2 2)')
],
'polygon' => [
'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))',
GeometryValueObject::fromWKT('POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))')
],
'multipoint' => [
'MULTIPOINT((0 0), (1 1))',
GeometryValueObject::fromWKT('MULTIPOINT((0 0), (1 1))')
],
];
}
public static function provideInvalidDatabaseValues(): array
{
return [
'integer' => [123],
'array' => [['invalid']],
'object' => [new \stdClass()],
'boolean' => [true],
'invalid wkt' => ['INVALID_WKT_STRING'],
'malformed point' => ['POINT(invalid coordinates)'],
];
}
public static function provideInvalidPHPValues(): array
{
return [
'string' => ['some string'],
'integer' => [123],
'array' => [['invalid']],
'object' => [new \stdClass()],
'boolean' => [true],
];
}
}
3. Value Object Tests (Following your Point example)
GeometryValueObjectTest.php
<?php
declare(strict_types=1);
namespace Tests\Unit\MartinGeorgiev\Doctrine\DBAL\Types\ValueObject;
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Geometry;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
class GeometryTest extends TestCase
{
#[DataProvider('provideValidWKT')]
#[Test]
public function can_create_from_wkt(string $wkt, string $expectedType, ?int $expectedSRID): void
{
$geometry = Geometry::fromWKT($wkt);
self::assertEquals($expectedType, $geometry->getGeometryType());
self::assertEquals($expectedSRID, $geometry->getSRID());
}
#[Test]
public function can_convert_to_string(): void
{
$wkt = 'POINT(1.23 4.56)';
$geometry = Geometry::fromWKT($wkt);
self::assertEquals($wkt, (string) $geometry);
}
#[Test]
public function preserves_srid_in_string_representation(): void
{
$wkt = 'SRID=4326;POINT(-122.4194 37.7749)';
$geometry = Geometry::fromWKT($wkt);
self::assertEquals($wkt, (string) $geometry);
}
#[DataProvider('provideGeometryEqualityTests')]
#[Test]
public function can_test_equality(Geometry $geometry1, Geometry $geometry2, bool $expectedEqual): void
{
self::assertEquals($expectedEqual, $geometry1->equals($geometry2));
}
#[DataProvider('provideCoordinateExtractionTests')]
#[Test]
public function can_extract_coordinates(string $wkt, array $expectedCoordinates): void
{
$geometry = Geometry::fromWKT($wkt);
self::assertEquals($expectedCoordinates, $geometry->getCoordinates());
}
#[DataProvider('provideInvalidWKT')]
#[Test]
public function throws_exception_for_invalid_wkt(string $invalidWkt): void
{
$this->expectException(\InvalidArgumentException::class);
Geometry::fromWKT($invalidWkt);
}
public static function provideValidWKT(): array
{
return [
'point' => ['POINT(1.23 4.56)', 'POINT', null],
'point with srid' => ['SRID=4326;POINT(-122.4194 37.7749)', 'POINT', 4326],
'linestring' => ['LINESTRING(0 0, 1 1, 2 2)', 'LINESTRING', null],
'polygon' => ['POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))', 'POLYGON', null],
'multipoint' => ['MULTIPOINT((0 0), (1 1))', 'MULTIPOINT', null],
'multilinestring' => ['MULTILINESTRING((0 0, 1 1), (2 2, 3 3))', 'MULTILINESTRING', null],
'multipolygon' => ['MULTIPOLYGON(((0 0, 0 1, 1 1, 1 0, 0 0)), ((2 2, 2 3, 3 3, 3 2, 2 2)))', 'MULTIPOLYGON', null],
'geometrycollection' => ['GEOMETRYCOLLECTION(POINT(0 0), LINESTRING(0 0, 1 1))', 'GEOMETRYCOLLECTION', null],
];
}
public static function provideGeometryEqualityTests(): array
{
return [
'same point' => [
Geometry::fromWKT('POINT(1 2)'),
Geometry::fromWKT('POINT(1 2)'),
true
],
'different points' => [
Geometry::fromWKT('POINT(1 2)'),
Geometry::fromWKT('POINT(3 4)'),
false
],
'same point different srid' => [
Geometry::fromWKT('SRID=4326;POINT(1 2)'),
Geometry::fromWKT('SRID=3857;POINT(1 2)'),
false
],
];
}
public static function provideCoordinateExtractionTests(): array
{
return [
'point' => ['POINT(1.23 4.56)', [[1.23, 4.56]]],
'linestring' => ['LINESTRING(0 0, 1 1, 2 2)', [[0, 0], [1, 1], [2, 2]]],
'polygon' => ['POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))', [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]]],
];
}
public static function provideInvalidWKT(): array
{
return [
'empty string' => [''],
'invalid geometry type' => ['INVALID(1 2)'],
'malformed coordinates' => ['POINT(invalid coordinates)'],
'missing coordinates' => ['POINT()'],
'unclosed polygon' => ['POLYGON((0 0, 0 1, 1 1))'],
];
}
}
4. Error Handling Pattern Tests (Following your exception structure)
Create comprehensive exception tests:
// InvalidGeometryForDatabaseExceptionTest.php
// InvalidGeometryForPHPExceptionTest.php
// InvalidGeographyForDatabaseExceptionTest.php
// etc.
5. Performance & Boundary Tests
#[Test]
public function handles_large_polygon_efficiently(): void
{
// Generate polygon with 1000+ vertices
$coordinates = [];
for ($i = 0; $i < 1000; $i++) {
$angle = ($i / 1000) * 2 * M_PI;
$x = cos($angle) * 100;
$y = sin($angle) * 100;
$coordinates[] = "$x $y";
}
$coordinates[] = $coordinates[0]; // Close the ring
$wkt = 'POLYGON((' . implode(', ', $coordinates) . '))';
$startTime = microtime(true);
$geometry = Geometry::fromWKT($wkt);
$endTime = microtime(true);
self::assertLessThan(1.0, $endTime - $startTime); // Should complete in under 1 second
self::assertEquals('POLYGON', $geometry->getGeometryType());
}
#[Test]
public function handles_coordinate_precision_correctly(): void
{
$highPrecisionWkt = 'POINT(1.123456789012345 2.987654321098765)';
$geometry = Geometry::fromWKT($highPrecisionWkt);
$coordinates = $geometry->getCoordinates();
self::assertEquals(1.123456789012345, $coordinates[0][0], '', 0.000000000000001);
self::assertEquals(2.987654321098765, $coordinates[0][1], '', 0.000000000000001);
}
6. SRID and Coordinate System Tests
#[DataProvider('provideSRIDTestCases')]
#[Test]
public function handles_different_srid_values(string $wkt, ?int $expectedSRID): void
{
$geometry = Geometry::fromWKT($wkt);
self::assertEquals($expectedSRID, $geometry->getSRID());
}
public static function provideSRIDTestCases(): array
{
return [
'no srid' => ['POINT(1 2)', null],
'wgs84' => ['SRID=4326;POINT(1 2)', 4326],
'web mercator' => ['SRID=3857;POINT(1 2)', 3857],
'utm zone 33n' => ['SRID=32633;POINT(1 2)', 32633],
];
}
This comprehensive test suite follows your existing patterns while providing thorough coverage for PostGIS functionality. The key aspects it covers:
- Database round-trip testing for all spatial types
- Value object validation and parsing
- Error handling with proper exception types
- Performance testing for large geometries
- SRID handling for different coordinate systems
- Array type support for spatial collections
- Boundary value testing for coordinate limits
Would you like me to elaborate on any specific aspect or help create additional test scenarios for particular PostGIS features?
Some support has just been added. Happy coding :)
Thank you! I can't wait for it to be released.