Skip to content

Commit 522e653

Browse files
refactor(martin-georgiev#305): move PostGIS operators in a dedicated namespace (martin-georgiev#433)
1 parent 5b5583b commit 522e653

File tree

55 files changed

+334
-89
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+334
-89
lines changed

docs/CONTRIBUTING.md

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ and can easily be run from project's root:
197197
- Run the full test suite:
198198
199199
```bash
200-
composer run-tests
200+
composer run-all-tests
201201
```
202202
203203
## Coding practices
@@ -253,3 +253,194 @@ class ArrayAppend extends BaseFunction
253253
⚠️ **Beware:** you cannot use **?** (e.g. the `??` operator) as part of any
254254
function prototype in Doctrine.
255255
It causes query parsing failures.
256+
257+
258+
## Testing: Patterns and Guidelines
259+
260+
This project has a rich, well-structured test suite consisting of fast unit tests and database-backed integration tests. Please follow the conventions below when adding or modifying tests.
261+
262+
### Tools and how to run tests
263+
- Framework: PHPUnit 10 (PHP attributes like #[Test], #[DataProvider])
264+
- Static analysis: PHPStan (+ doctrine + phpunit extensions)
265+
- Architecture checks: deptrac
266+
- Code style and refactoring: PHP-CS-Fixer, Rector
267+
268+
Composer scripts:
269+
- Run unit tests: `composer run-unit-tests` (uses ci/phpunit/config-unit.xml)
270+
- Run integration tests: `composer run-integration-tests` (uses ci/phpunit/config-integration.xml)
271+
- Run both suites: `composer run-all-tests`
272+
- Static analysis: `composer run-static-analysis`
273+
274+
Integration tests require a PostgreSQL with PostGIS:
275+
- Easiest: Docker Compose
276+
- Start: `docker-compose up -d`
277+
- Stop: `docker-compose down -v`
278+
- Alternatively (dev shell): `devenv up`
279+
- See tests/Integration/README.md for environment variables and details
280+
281+
Coverage reports are written to var/logs/test-coverage/{unit|integration}/.
282+
283+
### Choosing unit vs. integration tests
284+
- Prefer unit tests for:
285+
- Pure value objects and small utilities (no DB/ORM)
286+
- Doctrine DBAL Type conversions (PHP <-> database string) using an AbstractPlatform mock
287+
- DQL AST function SQL generation (no DB round-trip)
288+
- Prefer integration tests for:
289+
- Verifying DBAL types round-trip correctly against a real PostgreSQL
290+
- DQL functions/operators evaluated end-to-end against PostgreSQL
291+
- Scenarios relying on PostGIS or PostgreSQL-specific behavior
292+
293+
Keep unit tests fast and deterministic; use integration tests to validate behavior against the real database.
294+
295+
### Unit test patterns and conventions
296+
- Location: tests/Unit/...
297+
- Naming:
298+
- Class names end with `Test` (e.g., `PointTest`, `CidrTest`)
299+
- One file/class per subject
300+
- Concrete tests may be `final`
301+
- Structure:
302+
- Extend `PHPUnit\Framework\TestCase` or an existing base test class in Unit when available
303+
- Use `setUp()` to create an `AbstractPlatform` mock and the subject under test when testing DBAL Types
304+
- Use `#[DataProvider]` for bidirectional transformation scenarios (one provider used for both PHP->DB and DB->PHP tests)
305+
- Assertions:
306+
- Use domain-specific exceptions in negative tests (e.g., `InvalidCidrForPHPException`, `InvalidRangeForDatabaseException`)
307+
- Prefer dedicated assertion helpers provided by base classes (e.g., range equality helpers) when available
308+
- Value Object Range tests:
309+
- Reuse base classes:
310+
- `tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/BaseRangeTestCase`
311+
- `tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/BaseTimestampRangeTestCase`
312+
- Implement the abstract factory/expectation methods and provide concise data providers
313+
- DBAL Type unit tests:
314+
- Follow patterns from `PointTest`, `CidrTest`, `JsonbTest`
315+
- Test name retrieval (`getName()`), conversions in both directions, and invalid inputs
316+
- DQL AST unit tests:
317+
- Use `tests/Unit/.../ORM/Query/AST/Functions/TestCase` to assert DQL -> SQL transformation
318+
319+
Anti-patterns to avoid in unit tests:
320+
- No echo/print statements
321+
- Avoid Reflection; test through public APIs
322+
- Avoid raw/native SQL when verifying DBAL Types; prefer conversion tests with platform mock
323+
- Avoid excessive PHPStan suppression; prefer PHPDoc over `@phpstan-ignore`
324+
325+
### Integration test patterns and organization
326+
- Location: tests/Integration/...
327+
- Base infrastructure:
328+
- Extend `tests/Integration/MartinGeorgiev/TestCase`
329+
- Sets up Doctrine ORM config, connection, schema `test`, caches, and ensures PostGIS
330+
- Provides helpers to create/drop tables, run DQL, and assert results
331+
- Use specialized base classes based on what you test:
332+
- Array types: `ArrayTypeTestCase`
333+
- Scalar types: `ScalarTypeTestCase`
334+
- Range types: `RangeTypeTestCase` (includes operator tests and `assertRangeEquals`)
335+
- Spatial arrays: `SpatialArrayTypeTestCase` (ARRAY[...] insertion for WKT)
336+
- Per-type organization:
337+
- One integration test class per DBAL Type (e.g., `MacaddrTypeTest`, `JsonbTypeTest`, `IntegerArrayTypeTest`)
338+
- Implement:
339+
- `protected function getTypeName(): string` (Doctrine type name)
340+
- `protected function getPostgresTypeName(): string` (column type)
341+
- Data-driven tests:
342+
- Provide a `provideValidTransformations()` where applicable
343+
- Use `runDbalBindingRoundTrip($typeName, $columnType, $value)` for round-trips
344+
- Range integration tests:
345+
- Extend `RangeTypeTestCase`
346+
- Provide a data provider for range values and add `#[DataProvider('provideValidTransformations')]` to `can_handle_range_values()`
347+
- Optionally add operator scenarios via `provideOperatorScenarios()` returning [name, DQL, expectedIds]
348+
349+
Anti-patterns to avoid in integration tests:
350+
- Do not modify or rely on global/public schema; tests use the dedicated `test` schema created per test run
351+
- Avoid changing existing shared fixtures/data unless strictly necessary; prefer adding new, focused fixtures
352+
- No echo/print statements
353+
354+
### Base test classes and shared utilities
355+
Commonly used bases and what they provide:
356+
- Unit (Value Objects):
357+
- `BaseRangeTestCase`: creation/formatting/boundary tests with abstract factory methods
358+
- `BaseTimestampRangeTestCase`: extends the above with timestamp-specific boundary and helpers
359+
- Unit (DBAL Types):
360+
- `tests/Unit/.../DBAL/Types/BaseRangeTestCase`: negative cases and conversions for range DBAL types
361+
- Unit (ORM functions):
362+
- `tests/Unit/.../ORM/Query/AST/Functions/TestCase`: DQL to SQL transformation checks
363+
- Integration (DBAL Types):
364+
- `TestCase`: connection/schema setup, round-trip helper, assertions
365+
- `ArrayTypeTestCase`, `ScalarTypeTestCase`, `RangeTypeTestCase`, `SpatialArrayTypeTestCase`
366+
367+
Prefer extending these base classes over duplicating setup/utility code.
368+
369+
### Exception handling and error testing
370+
- Use domain-specific exceptions consistently:
371+
- `convertToPHPValue()` should throw `...ForPHPException`
372+
- `convertToDatabaseValue()` should throw `...ForDatabaseException`
373+
- In tests, assert the exact exception class and include `expectExceptionMessage()` when the message is part of the contract
374+
- For range equality in integration, use the provided `assertRangeEquals()` which compares string representation and emptiness
375+
376+
### Test data and fixtures
377+
- Entities for integration live under `fixtures/MartinGeorgiev/Doctrine/Entity` and are registered via attributes
378+
- If a new fixture is necessary, add it under the same namespace and keep it minimal and reusable
379+
- Range/operator tests seed their own tables (see `RangeTypeTestCase`’s `createRangeOperatorsTable()` and insert helpers)
380+
381+
### Code style in test files
382+
- Use PHP attributes `#[Test]` and `#[DataProvider]` (PHPUnit 10)
383+
- Descriptive dataset names in data providers improve failure readability
384+
- Prefer clear method names starting with verbs: `can_...`, `throws_...`, `dql_is_...`
385+
- Keep tests small and focused; avoid commentary that restates code; write comments only to explain intent or PostgreSQL-specific behavior
386+
- Maintain alphabetical order in documentation blocks and lists when applicable
387+
388+
### Minimal examples
389+
Unit test for a DBAL Type (mock platform, bidirectional conversions):
390+
391+
```php
392+
final class InetTest extends TestCase
393+
{
394+
/**
395+
* @var AbstractPlatform&MockObject
396+
*/
397+
private MockObject $platform;
398+
private Inet $fixture;
399+
400+
protected function setUp(): void
401+
{
402+
$this->platform = $this->createMock(AbstractPlatform::class);
403+
$this->fixture = new Inet();
404+
}
405+
406+
#[DataProvider('provideValidTransformations')]
407+
#[Test]
408+
public function can_transform_from_php_value(?string $php, ?string $pg): void
409+
{
410+
$this->assertEquals($pg, $this->fixture->convertToDatabaseValue($php, $this->platform));
411+
}
412+
413+
#[DataProvider('provideValidTransformations')]
414+
#[Test]
415+
public function can_transform_to_php_value(?string $php, ?string $pg): void
416+
{
417+
$this->assertEquals($php, $this->fixture->convertToPHPValue($pg, $this->platform));
418+
}
419+
}
420+
```
421+
422+
Integration test for an array type:
423+
424+
```php
425+
final class TextArrayTypeTest extends ArrayTypeTestCase
426+
{
427+
protected function getTypeName(): string { return 'text[]'; }
428+
protected function getPostgresTypeName(): string { return 'TEXT[]'; }
429+
#[DataProvider('provideValidTransformations')] #[Test]
430+
public function can_handle_array_values(string $name, array $value): void { parent::can_handle_array_values($name, $value); }
431+
}
432+
```
433+
434+
Range integration test:
435+
436+
```php
437+
final class Int4RangeTypeTest extends RangeTypeTestCase
438+
{
439+
protected function getTypeName(): string { return 'int4range'; }
440+
protected function getPostgresTypeName(): string { return 'INT4RANGE'; }
441+
#[DataProvider('provideValidTransformations')] #[Test]
442+
public function can_handle_range_values(string $name, RangeValueObject $range): void { parent::can_handle_range_values($name, $range); }
443+
}
444+
```
445+
446+
If unsure which base to extend or how to structure a new test, mirror a nearby, similar test and keep changes minimal and consistent with the patterns above.

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BoundingBoxDistance.php renamed to src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/PostGIS/BoundingBoxDistance.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\PostGIS;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction;
68

79
/**
810
* Implementation of PostGIS 2D bounding box distance operator (using <#>).

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/GeometryDistance.php renamed to src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/PostGIS/GeometryDistance.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\PostGIS;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction;
68

79
/**
810
* Implementation of PostGIS 2D distance between geometries operator (using <->).

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalBoundingBoxDistance.php renamed to src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/PostGIS/NDimensionalBoundingBoxDistance.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\PostGIS;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction;
68

79
/**
810
* Implementation of PostGIS n-D bounding box distance operator (using <<#>>).

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalCentroidDistance.php renamed to src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/PostGIS/NDimensionalCentroidDistance.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\PostGIS;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction;
68

79
/**
810
* Implementation of PostGIS n-D centroid distance operator (using <<->>).

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/NDimensionalOverlaps.php renamed to src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/PostGIS/NDimensionalOverlaps.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\PostGIS;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction;
68

79
/**
810
* Implementation of PostGIS n-dimensional bounding box intersects operator (using &&&).

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Overlaps.php renamed to src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/PostGIS/Overlaps.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\PostGIS;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction;
68

79
/**
810
* Implementation of PostgreSQL check if left side overlaps with right side (using &&).

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsAbove.php renamed to src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/PostGIS/OverlapsAbove.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\PostGIS;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction;
68

79
/**
810
* Implementation of PostGIS bounding box overlaps or is above operator (using |&>).

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsBelow.php renamed to src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/PostGIS/OverlapsBelow.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\PostGIS;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction;
68

79
/**
810
* Implementation of PostGIS bounding box overlaps or is below operator (using &<|).

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/OverlapsLeft.php renamed to src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/PostGIS/OverlapsLeft.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\PostGIS;
6+
7+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseFunction;
68

79
/**
810
* Implementation of PostGIS bounding box overlaps or is to the left operator (using &<).

0 commit comments

Comments
 (0)