diff --git a/.github/workflows/pest-coverage.yml b/.github/workflows/pest-coverage.yml index 37948e3..73adb5d 100644 --- a/.github/workflows/pest-coverage.yml +++ b/.github/workflows/pest-coverage.yml @@ -1,6 +1,10 @@ name: Tests coverage -on: [ push, pull_request ] +on: + push: + branches: + - master + pull_request: jobs: test: diff --git a/.github/workflows/pest.yml b/.github/workflows/pest.yml index ac1c1c7..19c10fd 100644 --- a/.github/workflows/pest.yml +++ b/.github/workflows/pest.yml @@ -1,6 +1,10 @@ name: Tests -on: [ push, pull_request ] +on: + push: + branches: + - master + pull_request: jobs: test: @@ -13,8 +17,8 @@ jobs: matrix: php: [ 8.3, 8.2, 8.1 ] laravel: [ 10.* ] - db: [ 'mysql:8.0', 'mysql:5.7', 'mariadb:10.9' ] - dependency-version: [ prefer-lowest, prefer-stable ] + db: [ 'mysql:8.0', 'mysql:5.7', 'mariadb:10.11', 'postgis/postgis:16-3.4', 'postgis/postgis:15-3.4', 'postgis/postgis:14-3.4', 'postgis/postgis:13-3.4', 'postgis/postgis:12-3.4' ] + dependency-version: [ prefer-stable, prefer-lowest ] include: - laravel: 10.* testbench: ^8.0 @@ -25,9 +29,14 @@ jobs: env: MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_DATABASE: laravel_eloquent_spatial_test + POSTGRES_DB: laravel_eloquent_spatial_test + POSTGRES_USER: root + POSTGRES_HOST_AUTH_METHOD: trust ports: - - 3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + - ${{ contains(matrix.db, 'postgis') && '5432' || '3306' }} + options: >- + ${{ (contains(matrix.db, 'postgis') && '--health-cmd="pg_isready"') || '--health-cmd="mysqladmin ping"' }} + --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout code @@ -46,5 +55,6 @@ jobs: - name: Execute tests env: - DB_PORT: ${{ job.services.db.ports['3306'] }} + DB_PORT: ${{ job.services.db.ports[contains(matrix.db, 'postgis') && '5432' || '3306'] }} + DB_CONNECTION: ${{ contains(matrix.db, 'postgis') && 'pgsql' || 'mysql' }} run: vendor/bin/pest diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index 34d897e..66cb0b0 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -1,6 +1,10 @@ name: Lint -on: [ push, pull_request ] +on: + push: + branches: + - master + pull_request: jobs: php-cs-fixer: diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index cbed36e..f192975 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -1,6 +1,10 @@ name: Static code analysis -on: [ push, pull_request ] +on: + push: + branches: + - master + pull_request: jobs: phpstan: diff --git a/.gitignore b/.gitignore index c573a05..476b42d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea .php-cs-fixer.cache +.phpunit.cache .phpunit.result.cache build composer.lock diff --git a/.run/Test.run.xml b/.run/Test - MySQL 8.0.run.xml similarity index 62% rename from .run/Test.run.xml rename to .run/Test - MySQL 8.0.run.xml index 4fc55d4..21b7dbe 100644 --- a/.run/Test.run.xml +++ b/.run/Test - MySQL 8.0.run.xml @@ -1,5 +1,10 @@ - + + + + + + diff --git a/.run/Test - Postgres.run.xml b/.run/Test - Postgres.run.xml new file mode 100644 index 0000000..f068976 --- /dev/null +++ b/.run/Test - Postgres.run.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 74b2ac2..108f28b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,10 @@ **This Laravel package allows you to easily work with spatial data types and functions.** -This package supports MySQL v8, MySQL v5.7, and MariaDB v10. +Supported databases: +- MySQL 5.7/8 +- MariaDB 10 +- Postgres 12/13/14/15/16 with PostGIS 3.4 ## Getting Started @@ -177,8 +180,8 @@ echo $londonEyePoint->getName(); // Point Here are some useful commands for development: -* Run tests: `composer pest` -* Run tests with coverage: `composer pest-coverage` +* Run tests: `composer pest:mysql` or `composer pest:postgres` +* Run tests with coverage: `composer pest-coverage:mysql` * Perform type checking: `composer phpstan` * Format your code: `composer php-cs-fixer` diff --git a/composer.json b/composer.json index d94fa8d..2ca27cb 100644 --- a/composer.json +++ b/composer.json @@ -38,8 +38,9 @@ "scripts": { "php-cs-fixer": "PHP_CS_FIXER_IGNORE_ENV=1 ./vendor/bin/php-cs-fixer fix --allow-risky=yes", "phpstan": "./vendor/bin/phpstan analyse --memory-limit=2G", - "pest": "./vendor/bin/pest", - "pest-coverage": "XDEBUG_MODE=coverage ./vendor/bin/pest --coverage --min=100" + "pest:mysql": "DB_PORT=3307 ./vendor/bin/pest", + "pest:postgres": "DB_CONNECTION=pgsql DB_PORT=5433 ./vendor/bin/pest", + "pest-coverage:mysql": "XDEBUG_MODE=coverage DB_PORT=3307 ./vendor/bin/pest --coverage --min=100" }, "config": { "sort-packages": true, diff --git a/docker-compose.yaml b/docker-compose.yaml index aa97241..2127b24 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ version: '3.8' services: - db: + mysql: container_name: mysql-laravel-eloquent-spatial image: mysql:8.0 environment: @@ -10,8 +10,21 @@ services: volumes: - mysql_data:/var/lib/mysql ports: - - 3306:3306 + - 3307:3306 + restart: unless-stopped + postgres: + container_name: postgres-laravel-eloquent-spatial + image: postgis/postgis:16-2 + environment: + POSTGRES_DB: laravel_eloquent_spatial_test + POSTGRES_USER: root + POSTGRES_HOST_AUTH_METHOD: trust + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - 5433:5432 restart: unless-stopped volumes: mysql_data: + postgres_data: diff --git a/phpstan.neon b/phpstan.neon index e4e52d3..2d898cb 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,6 +6,17 @@ parameters: - tests excludePaths: - src/Factory.php + ignoreErrors: + - + message: '#Call to an undefined method Illuminate\\Database\\Schema\\ColumnDefinition\:\:(isGeometry|projection)\(\)#' + path: tests/database/migrations/*.php + - + message: '#Undefined variable: \$this#' + path: tests/Expectations.php + - + message: '#Call to an undefined method Pest\\Expectation\<.+\>\:\:toBe(InstanceOf)?On(Postgres|Mysql)\(\)#' + path: tests/*.php + level: max checkMissingIterableValueType: true checkGenericClassInNonGenericObjectType: false diff --git a/src/AxisOrder.php b/src/AxisOrder.php index 11e991b..030fb37 100644 --- a/src/AxisOrder.php +++ b/src/AxisOrder.php @@ -8,40 +8,40 @@ use Illuminate\Database\MySqlConnection; use PDO; +/** @codeCoverageIgnore */ class AxisOrder { - public function __construct() + public static function supported(ConnectionInterface $connection): bool { - } - - public function supported(ConnectionInterface $connection): bool - { - /** @var MySqlConnection $connection */ - if ($this->isMariaDb($connection)) { - // @codeCoverageIgnoreStart + if (self::isMariaDb($connection)) { return false; - // @codeCoverageIgnoreEnd } - if ($this->isMySql57($connection)) { - // @codeCoverageIgnoreStart - return false; - // @codeCoverageIgnoreEnd + if (self::isMySql8OrAbove($connection)) { + return true; } - return true; + return false; } - private function isMariaDb(MySqlConnection $connection): bool + private static function isMariaDb(ConnectionInterface $connection): bool { + if (! ($connection instanceof MySqlConnection)) { + return false; + } + return $connection->isMaria(); } - private function isMySql57(MySqlConnection $connection): bool + private static function isMySql8OrAbove(ConnectionInterface $connection): bool { + if (! ($connection instanceof MySqlConnection)) { + return false; + } + /** @var string $version */ $version = $connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION); - return version_compare($version, '5.8.0', '<'); + return version_compare($version, '8.0.0', '>='); } } diff --git a/src/Doctrine/GeographyType.php b/src/Doctrine/GeographyType.php new file mode 100644 index 0000000..a65508a --- /dev/null +++ b/src/Doctrine/GeographyType.php @@ -0,0 +1,22 @@ + MultiPolygonType::class, 'geometrycollection' => GeometryCollectionType::class, 'geomcollection' => GeometryCollectionType::class, + 'geography' => GeographyType::class, + 'geometry' => GeometryType::class, ]; foreach ($geometries as $type => $class) { diff --git a/src/GeometryExpression.php b/src/GeometryExpression.php new file mode 100644 index 0000000..d1d7ffd --- /dev/null +++ b/src/GeometryExpression.php @@ -0,0 +1,23 @@ +expression.'::geometry' + : $this->expression; + } +} diff --git a/src/Objects/Geometry.php b/src/Objects/Geometry.php index 37f24b7..c09fa8a 100644 --- a/src/Objects/Geometry.php +++ b/src/Objects/Geometry.php @@ -20,6 +20,7 @@ use MatanYadaev\EloquentSpatial\Enums\Srid; use MatanYadaev\EloquentSpatial\Factory; use MatanYadaev\EloquentSpatial\GeometryCast; +use MatanYadaev\EloquentSpatial\GeometryExpression; use Stringable; use WKB as geoPHPWkb; @@ -67,19 +68,23 @@ public function toWkb(): string /** * @param string $wkb * @return static - * - * @throws InvalidArgumentException */ public static function fromWkb(string $wkb): static { - $srid = substr($wkb, 0, 4); - // @phpstan-ignore-next-line - $srid = unpack('L', $srid)[1]; + if (ctype_xdigit($wkb)) { + // @codeCoverageIgnoreStart + $geometry = Factory::parse($wkb); + // @codeCoverageIgnoreEnd + } else { + $srid = substr($wkb, 0, 4); + // @phpstan-ignore-next-line + $srid = unpack('L', $srid)[1]; - $wkb = substr($wkb, 4); + $wkb = substr($wkb, 4); - $geometry = Factory::parse($wkb); - $geometry->srid = $srid; + $geometry = Factory::parse($wkb); + $geometry->srid = $srid; + } if (! ($geometry instanceof static)) { throw new InvalidArgumentException( @@ -217,12 +222,12 @@ public function toSqlExpression(ConnectionInterface $connection): ExpressionCont { $wkt = $this->toWkt(); - if (! (new AxisOrder)->supported($connection)) { + if (! AxisOrder::supported($connection)) { // @codeCoverageIgnoreStart - return DB::raw("ST_GeomFromText('{$wkt}', {$this->srid})"); + return DB::raw((new GeometryExpression("ST_GeomFromText('{$wkt}', {$this->srid})"))->normalize($connection)); // @codeCoverageIgnoreEnd } - return DB::raw("ST_GeomFromText('{$wkt}', {$this->srid}, 'axis-order=long-lat')"); + return DB::raw((new GeometryExpression("ST_GeomFromText('{$wkt}', {$this->srid}, 'axis-order=long-lat')"))->normalize($connection)); } } diff --git a/src/Traits/HasSpatial.php b/src/Traits/HasSpatial.php index ba7b271..019915e 100644 --- a/src/Traits/HasSpatial.php +++ b/src/Traits/HasSpatial.php @@ -4,7 +4,9 @@ use Illuminate\Contracts\Database\Query\Expression as ExpressionContract; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\PostgresConnection; use Illuminate\Support\Facades\DB; +use MatanYadaev\EloquentSpatial\GeometryExpression; use MatanYadaev\EloquentSpatial\Objects\Geometry; trait HasSpatial @@ -73,9 +75,16 @@ public function scopeWithDistanceSphere( $query->select('*'); } + // @codeCoverageIgnoreStart + $function = $this->getConnection() instanceof PostgresConnection + ? 'ST_DistanceSphere' + : 'ST_DISTANCE_SPHERE'; + // @codeCoverageIgnoreEnd + $query->selectRaw( sprintf( - 'ST_DISTANCE_SPHERE(%s, %s) AS %s', + '%s(%s, %s) AS %s', + $function, $this->toExpressionString($column), $this->toExpressionString($geometryOrColumn), $alias, @@ -90,9 +99,16 @@ public function scopeWhereDistanceSphere( string $operator, int|float $value ): void { + // @codeCoverageIgnoreStart + $function = $this->getConnection() instanceof PostgresConnection + ? 'ST_DistanceSphere' + : 'ST_DISTANCE_SPHERE'; + // @codeCoverageIgnoreEnd + $query->whereRaw( sprintf( - 'ST_DISTANCE_SPHERE(%s, %s) %s ?', + '%s(%s, %s) %s ?', + $function, $this->toExpressionString($column), $this->toExpressionString($geometryOrColumn), $operator, @@ -107,9 +123,16 @@ public function scopeOrderByDistanceSphere( ExpressionContract|Geometry|string $geometryOrColumn, string $direction = 'asc' ): void { + // @codeCoverageIgnoreStart + $function = $this->getConnection() instanceof PostgresConnection + ? 'ST_DistanceSphere' + : 'ST_DISTANCE_SPHERE'; + // @codeCoverageIgnoreEnd + $query->orderByRaw( sprintf( - 'ST_DISTANCE_SPHERE(%s, %s) %s', + '%s(%s, %s) %s', + $function, $this->toExpressionString($column), $this->toExpressionString($geometryOrColumn), $direction @@ -136,11 +159,14 @@ public function scopeWhereNotWithin( ExpressionContract|Geometry|string $column, ExpressionContract|Geometry|string $geometryOrColumn, ): void { + $value = $this->getConnection() instanceof PostgresConnection ? 'false' : 0; + $query->whereRaw( sprintf( - 'ST_WITHIN(%s, %s) = 0', + 'ST_WITHIN(%s, %s) = %s', $this->toExpressionString($column), $this->toExpressionString($geometryOrColumn), + $value, ) ); } @@ -164,11 +190,14 @@ public function scopeWhereNotContains( ExpressionContract|Geometry|string $column, ExpressionContract|Geometry|string $geometryOrColumn, ): void { + $value = $this->getConnection() instanceof PostgresConnection ? 'false' : 0; + $query->whereRaw( sprintf( - 'ST_CONTAINS(%s, %s) = 0', + 'ST_CONTAINS(%s, %s) = %s', $this->toExpressionString($column), $this->toExpressionString($geometryOrColumn), + $value, ) ); } @@ -294,9 +323,11 @@ protected function toExpressionString(ExpressionContract|Geometry|string $geomet if ($geometryOrColumnOrExpression instanceof ExpressionContract) { $expression = $geometryOrColumnOrExpression; } elseif ($geometryOrColumnOrExpression instanceof Geometry) { - $expression = $geometryOrColumnOrExpression->toSqlExpression($this->getConnection()); + $expression = DB::raw($geometryOrColumnOrExpression->toSqlExpression($this->getConnection())->getValue($grammar)); } else { - $expression = DB::raw($grammar->wrap($geometryOrColumnOrExpression)); + $expression = DB::raw( + (new GeometryExpression($grammar->wrap($geometryOrColumnOrExpression)))->normalize($this->getConnection()) + ); } return (string) $expression->getValue($grammar); diff --git a/tests/DoctrineTypesTest.php b/tests/DoctrineTypesTest.php index 4967b7c..8ce2676 100644 --- a/tests/DoctrineTypesTest.php +++ b/tests/DoctrineTypesTest.php @@ -2,7 +2,9 @@ use Doctrine\DBAL\Types\Type; use Illuminate\Support\Facades\DB; +use MatanYadaev\EloquentSpatial\Doctrine\GeographyType; use MatanYadaev\EloquentSpatial\Doctrine\GeometryCollectionType; +use MatanYadaev\EloquentSpatial\Doctrine\GeometryType; use MatanYadaev\EloquentSpatial\Doctrine\LineStringType; use MatanYadaev\EloquentSpatial\Doctrine\MultiLineStringType; use MatanYadaev\EloquentSpatial\Doctrine\MultiPointType; @@ -10,20 +12,54 @@ use MatanYadaev\EloquentSpatial\Doctrine\PointType; use MatanYadaev\EloquentSpatial\Doctrine\PolygonType; -it('uses custom Doctrine types for spatial columns', function (string $column, string $typeClass, string $typeName): void { - /** @var class-string $typeClass */ +/** @var array{column: string, postgresType: class-string, mySqlType: class-string} $typeClass */ +$dataset = [ + [ + 'column' => 'point', + 'postgresType' => GeometryType::class, + 'mySqlType' => PointType::class, + ], + [ + 'column' => 'point_geography', + 'postgresType' => GeographyType::class, + 'mySqlType' => PointType::class, + ], + [ + 'column' => 'line_string', + 'postgresType' => GeometryType::class, + 'mySqlType' => LineStringType::class, + ], + [ + 'column' => 'multi_point', + 'postgresType' => GeometryType::class, + 'mySqlType' => MultiPointType::class, + ], + [ + 'column' => 'polygon', + 'postgresType' => GeometryType::class, + 'mySqlType' => PolygonType::class, + ], + [ + 'column' => 'multi_line_string', + 'postgresType' => GeometryType::class, + 'mySqlType' => MultiLineStringType::class, + ], + [ + 'column' => 'multi_polygon', + 'postgresType' => GeometryType::class, + 'mySqlType' => MultiPolygonType::class, + ], + [ + 'column' => 'geometry_collection', + 'postgresType' => GeometryType::class, + 'mySqlType' => GeometryCollectionType::class, + ], +]; + +it('uses custom Doctrine types for spatial columns', function ($column, $postgresType, $mySqlType): void { $doctrineSchemaManager = DB::connection()->getDoctrineSchemaManager(); $columns = $doctrineSchemaManager->listTableColumns('test_places'); - expect($columns[$column]->getType())->toBeInstanceOf($typeClass) - ->and($columns[$column]->getType()->getName())->toBe($typeName); -})->with([ - 'point' => ['point', PointType::class, 'point'], - 'line_string' => ['line_string', LineStringType::class, 'linestring'], - 'multi_point' => ['multi_point', MultiPointType::class, 'multipoint'], - 'polygon' => ['polygon', PolygonType::class, 'polygon'], - 'multi_line_string' => ['multi_line_string', MultiLineStringType::class, 'multilinestring'], - 'multi_polygon' => ['multi_polygon', MultiPolygonType::class, 'multipolygon'], - 'geometry_collection' => ['geometry_collection', GeometryCollectionType::class, 'geometrycollection'], -]); + expect($columns[$column]->getType())->toBeInstanceOfOnPostgres($postgresType)->toBeInstanceOfOnMysql($mySqlType); +})->with($dataset); diff --git a/tests/Expectations.php b/tests/Expectations.php new file mode 100644 index 0000000..1620bdd --- /dev/null +++ b/tests/Expectations.php @@ -0,0 +1,20 @@ +extend('toBeOnPostgres', function (mixed $value) { + return $this->when(DB::connection() instanceof PostgresConnection, fn () => $this->toBe($value)); +}); + +expect()->extend('toBeOnMysql', function (mixed $value) { + return $this->when(! (DB::connection() instanceof PostgresConnection), fn () => $this->toBe($value)); +}); + +expect()->extend('toBeInstanceOfOnPostgres', function (string $type) { + return $this->when(DB::connection() instanceof PostgresConnection, fn () => $this->toBeInstanceOf($type)); +}); + +expect()->extend('toBeInstanceOfOnMysql', function (string $type) { + return $this->when(! (DB::connection() instanceof PostgresConnection), fn () => $this->toBeInstanceOf($type)); +}); diff --git a/tests/GeometryCastTest.php b/tests/GeometryCastTest.php index 4a6bb27..070ce4d 100644 --- a/tests/GeometryCastTest.php +++ b/tests/GeometryCastTest.php @@ -1,14 +1,12 @@ create(['point' => null]); @@ -107,7 +105,9 @@ it('throws exception when cast deserializing incorrect geometry object', function (): void { TestPlace::insert(array_merge(TestPlace::factory()->definition(), [ - 'point_with_line_string_cast' => DB::raw('POINT(0, 180)'), + 'point_with_line_string_cast' => DB::raw( + (new GeometryExpression('POINT(0, 180)'))->normalize(DB::connection()) + ), ])); /** @var TestPlace $testPlace */ $testPlace = TestPlace::firstOrFail(); diff --git a/tests/SpatialBuilderTest.php b/tests/HasSpatialTest.php similarity index 92% rename from tests/SpatialBuilderTest.php rename to tests/HasSpatialTest.php index 9bee2bb..0e69fd8 100644 --- a/tests/SpatialBuilderTest.php +++ b/tests/HasSpatialTest.php @@ -1,16 +1,14 @@ create(['point' => new Point(0, 0, Srid::WGS84->value)]); @@ -20,7 +18,7 @@ ->firstOrFail(); expect($testPlaceWithDistance->distance)->toBe(156897.79947260793); -})->skip(fn () => ! (new AxisOrder)->supported(DB::connection())); +})->skip(fn () => ! AxisOrder::supported(DB::connection())); it('calculates distance - without axis-order', function (): void { TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); @@ -31,7 +29,7 @@ ->firstOrFail(); expect($testPlaceWithDistance->distance)->toBe(1.4142135623730951); -})->skip(fn () => (new AxisOrder)->supported(DB::connection())); +})->skip(fn () => AxisOrder::supported(DB::connection())); it('calculates distance with alias', function (): void { TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); @@ -42,7 +40,7 @@ ->firstOrFail(); expect($testPlaceWithDistance->distance_in_meters)->toBe(156897.79947260793); -})->skip(fn () => ! (new AxisOrder)->supported(DB::connection())); +})->skip(fn () => ! AxisOrder::supported(DB::connection())); it('calculates distance with alias - without axis-order', function (): void { TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); @@ -53,7 +51,7 @@ ->firstOrFail(); expect($testPlaceWithDistance->distance_in_meters)->toBe(1.4142135623730951); -})->skip(fn () => (new AxisOrder)->supported(DB::connection())); +})->skip(fn () => AxisOrder::supported(DB::connection())); it('filters by distance', function (): void { $pointWithinDistance = new Point(0, 0, Srid::WGS84->value); @@ -68,7 +66,7 @@ expect($testPlacesWithinDistance)->toHaveCount(1); expect($testPlacesWithinDistance[0]->point)->toEqual($pointWithinDistance); -})->skip(fn () => ! (new AxisOrder)->supported(DB::connection())); +})->skip(fn () => ! AxisOrder::supported(DB::connection())); it('filters by distance - without axis-order', function (): void { $pointWithinDistance = new Point(0, 0, Srid::WGS84->value); @@ -83,7 +81,7 @@ expect($testPlacesWithinDistance)->toHaveCount(1); expect($testPlacesWithinDistance[0]->point)->toEqual($pointWithinDistance); -})->skip(fn () => (new AxisOrder)->supported(DB::connection())); +})->skip(fn () => AxisOrder::supported(DB::connection())); it('orders by distance ASC', function (): void { $closerTestPlace = TestPlace::factory()->create(['point' => new Point(1, 1, Srid::WGS84->value)]); @@ -120,7 +118,7 @@ ->firstOrFail(); expect($testPlaceWithDistance->distance)->toBe(157249.59776850493); -})->skip(fn () =>! (new AxisOrder)->supported(DB::connection())); +})->skip(fn () =>! AxisOrder::supported(DB::connection())); it('calculates distance sphere - without axis-order', function (): void { TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); @@ -130,8 +128,9 @@ ->withDistanceSphere('point', new Point(1, 1, Srid::WGS84->value)) ->firstOrFail(); - expect($testPlaceWithDistance->distance)->toBe(157249.0357231545); -})->skip(fn () => (new AxisOrder)->supported(DB::connection())); + expect($testPlaceWithDistance->distance)->toBeOnPostgres(157249.59776851); + expect($testPlaceWithDistance->distance)->toBeOnMysql(157249.0357231545); +})->skip(fn () => AxisOrder::supported(DB::connection())); it('calculates distance sphere with alias', function (): void { TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); @@ -142,7 +141,7 @@ ->firstOrFail(); expect($testPlaceWithDistance->distance_in_meters)->toBe(157249.59776850493); -})->skip(fn () => ! (new AxisOrder)->supported(DB::connection())); +})->skip(fn () => ! AxisOrder::supported(DB::connection())); it('calculates distance sphere with alias - without axis-order', function (): void { TestPlace::factory()->create(['point' => new Point(0, 0, Srid::WGS84->value)]); @@ -152,8 +151,9 @@ ->withDistanceSphere('point', new Point(1, 1, Srid::WGS84->value), 'distance_in_meters') ->firstOrFail(); - expect($testPlaceWithDistance->distance_in_meters)->toBe(157249.0357231545); -})->skip(fn () => (new AxisOrder)->supported(DB::connection())); + expect($testPlaceWithDistance->distance_in_meters)->toBeOnPostgres(157249.59776851); + expect($testPlaceWithDistance->distance_in_meters)->toBeOnMysql(157249.0357231545); +})->skip(fn () => AxisOrder::supported(DB::connection())); it('filters distance sphere', function (): void { $pointWithinDistance = new Point(0, 0, Srid::WGS84->value); @@ -435,10 +435,12 @@ 'longitude' => 0, 'latitude' => 0, ]); + $expression = DB::raw((new GeometryExpression('POINT(longitude, latitude)'))->normalize(DB::connection())); + $expression2 = DB::raw((new GeometryExpression('polygon'))->normalize(DB::connection())); /** @var TestPlace $testPlaceWithDistance */ $testPlaceWithDistance = TestPlace::query() - ->whereWithin(DB::raw('POINT(longitude, latitude)'), DB::raw('polygon')) + ->whereWithin($expression, $expression2) ->firstOrFail(); expect($testPlaceWithDistance)->not()->toBeNull(); @@ -454,14 +456,14 @@ }); it('toExpressionString can handle a Geometry input', function (): void { - $spatialBuilder = new TestPlace(); - $toExpressionStringMethod = (new ReflectionClass($spatialBuilder))->getMethod('toExpressionString'); + $testPlace = new TestPlace(); + $toExpressionStringMethod = (new ReflectionClass($testPlace))->getMethod('toExpressionString'); $polygon = Polygon::fromJson('{"type":"Polygon","coordinates":[[[-1,-1],[1,-1],[1,1],[-1,1],[-1,-1]]]}'); - $result = $toExpressionStringMethod->invoke($spatialBuilder, $polygon); + $result = $toExpressionStringMethod->invoke($testPlace, $polygon); - $grammar = $spatialBuilder->getGrammar(); - $connection = $spatialBuilder->getConnection(); + $grammar = $testPlace->getGrammar(); + $connection = $testPlace->getConnection(); $sqlSerializedPolygon = $polygon->toSqlExpression($connection)->getValue($grammar); expect($result)->toBe($sqlSerializedPolygon); }); @@ -472,5 +474,6 @@ $result = $toExpressionStringMethod->invoke($spatialBuilder, 'test_places.point'); - expect($result)->toBe('`test_places`.`point`'); + expect($result)->toBeOnPostgres('"test_places"."point"::geometry'); + expect($result)->toBeOnMysql('`test_places`.`point`'); }); diff --git a/tests/Objects/GeometryCollectionTest.php b/tests/Objects/GeometryCollectionTest.php index c32bc81..0bea357 100644 --- a/tests/Objects/GeometryCollectionTest.php +++ b/tests/Objects/GeometryCollectionTest.php @@ -1,6 +1,5 @@ value)); TestPlace::factory()->create(['point' => $point]); })->toThrow(QueryException::class); -})->skip(fn () => ! (new AxisOrder)->supported(DB::connection())); +})->skip(fn () => ! AxisOrder::supported(DB::connection())); it('throws exception when generating geometry with invalid latitude - without axis-order', function (): void { expect(function (): void { @@ -33,14 +35,14 @@ ->withDistanceSphere('point', new Point(1, 1, Srid::WGS84->value)) ->firstOrFail(); })->toThrow(QueryException::class); -})->skip(fn () => (new AxisOrder)->supported(DB::connection())); +})->skip(fn () => AxisOrder::supported(DB::connection()) || DB::connection() instanceof PostgresConnection); it('throws exception when generating geometry with invalid longitude', function (): void { expect(function (): void { $point = (new Point(0, 181, Srid::WGS84->value)); TestPlace::factory()->create(['point' => $point]); })->toThrow(QueryException::class); -})->skip(fn () => ! (new AxisOrder)->supported(DB::connection())); +})->skip(fn () => ! AxisOrder::supported(DB::connection())); it('throws exception when generating geometry with invalid longitude - without axis-order', function (): void { expect(function (): void { @@ -51,7 +53,7 @@ ->withDistanceSphere('point', new Point(1, 1, Srid::WGS84->value)) ->firstOrFail(); })->toThrow(QueryException::class); -})->skip(fn () => (new AxisOrder)->supported(DB::connection())); +})->skip(fn () => AxisOrder::supported(DB::connection()) || DB::connection() instanceof PostgresConnection); it('throws exception when generating geometry from other geometry WKT', function (): void { expect(function (): void { @@ -89,7 +91,7 @@ $grammar = DB::getQueryGrammar(); $expressionValue = $expression->getValue($grammar); expect($expressionValue)->toEqual("ST_GeomFromText('POINT(180 0)', 4326, 'axis-order=long-lat')"); -})->skip(fn () => ! (new AxisOrder)->supported(DB::connection())); +})->skip(fn () => ! AxisOrder::supported(DB::connection())); it('creates an SQL expression from a geometry - without axis-order', function (): void { $point = new Point(0, 180, Srid::WGS84->value); @@ -98,8 +100,10 @@ $grammar = DB::getQueryGrammar(); $expressionValue = $expression->getValue($grammar); - expect($expressionValue)->toEqual("ST_GeomFromText('POINT(180 0)', 4326)"); -})->skip(fn () => (new AxisOrder)->supported(DB::connection())); + expect($expressionValue)->toEqual( + (new GeometryExpression("ST_GeomFromText('POINT(180 0)', 4326)"))->normalize(DB::connection()) + ); +})->skip(fn () => AxisOrder::supported(DB::connection())); it('creates a geometry object from a geo json array', function (): void { $point = new Point(0, 180); diff --git a/tests/Objects/LineStringTest.php b/tests/Objects/LineStringTest.php index 13668b2..3256ec2 100644 --- a/tests/Objects/LineStringTest.php +++ b/tests/Objects/LineStringTest.php @@ -1,6 +1,5 @@ in(__DIR__); + +uses(RefreshDatabase::class)->in(__DIR__); diff --git a/tests/TestModels/TestPlace.php b/tests/TestModels/TestPlace.php index de97b40..57392b7 100644 --- a/tests/TestModels/TestPlace.php +++ b/tests/TestModels/TestPlace.php @@ -53,6 +53,8 @@ class TestPlace extends Model 'multi_polygon' => MultiPolygon::class, 'geometry_collection' => GeometryCollection::class, 'point_with_line_string_cast' => LineString::class, + 'distance' => 'float', + 'distance_in_meters' => 'float', ]; protected static function newFactory(): TestPlaceFactory diff --git a/tests/database/migrations/0000_00_00_000000_create_test_places_table.php b/tests/database/migrations/0000_00_00_000000_create_test_places_table.php index 6d66fb1..06b0c32 100644 --- a/tests/database/migrations/0000_00_00_000000_create_test_places_table.php +++ b/tests/database/migrations/0000_00_00_000000_create_test_places_table.php @@ -13,14 +13,15 @@ public function up(): void $table->timestamps(); $table->string('name'); $table->string('address'); - $table->point('point')->nullable(); - $table->multiPoint('multi_point')->nullable(); - $table->lineString('line_string')->nullable(); - $table->multiLineString('multi_line_string')->nullable(); - $table->polygon('polygon')->nullable(); - $table->multiPolygon('multi_polygon')->nullable(); - $table->geometryCollection('geometry_collection')->nullable(); - $table->point('point_with_line_string_cast')->nullable(); + $table->point('point')->isGeometry()->projection(0)->nullable(); + $table->multiPoint('multi_point')->isGeometry()->projection(0)->nullable(); + $table->lineString('line_string')->isGeometry()->projection(0)->nullable(); + $table->multiLineString('multi_line_string')->isGeometry()->projection(0)->nullable(); + $table->polygon('polygon')->isGeometry()->projection(0)->nullable(); + $table->multiPolygon('multi_polygon')->isGeometry()->projection(0)->nullable(); + $table->geometryCollection('geometry_collection')->isGeometry()->projection(0)->nullable(); + $table->point('point_with_line_string_cast')->isGeometry()->projection(0)->nullable(); + $table->point('point_geography')->nullable(); $table->decimal('longitude')->nullable(); $table->decimal('latitude')->nullable(); });