diff --git a/README.md b/README.md index 5b2ea74..c7ac105 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ [![Total Downloads](https://img.shields.io/packagist/dt/tarfin-labs/laravel-spatial.svg?style=flat-square)](https://packagist.org/packages/tarfin-labs/laravel-spatial) ![GitHub Actions](https://github.com/tarfin-labs/laravel-spatial/actions/workflows/main.yml/badge.svg) -Laravel package to work with geospatial data types and functions. +This is a Laravel package to work with geospatial data types and functions. -For now, it supports only MySql Spatial Data Types and Functions. +It supports only MySQL Spatial Data Types and Functions, other RDBMS is on the roadmap. **Supported data types:** - `Point` @@ -57,18 +57,22 @@ return new class extends SpatialMigration { The migration above creates an `addresses` table with a `location` spatial column. ->Spatial columns with no SRID attribute are not SRID-restricted and accept values with any SRID. However, the optimizer cannot use SPATIAL indexes on them until the column definition is modified to include an SRID attribute, which may require that the column contents first be modified so that all values have the same SRID. +> Spatial columns with no SRID attribute are not SRID-restricted and accept values with any SRID. However, the optimizer cannot use SPATIAL indexes on them until the column definition is modified to include an SRID attribute, which may require that the column contents first be modified so that all values have the same SRID. So you should give an SRID attribute to use spatial indexes in the migrations: + ```php Schema::create('addresses', function (Blueprint $table) { - $table->point('location', 4326); + $table->point(column: 'location', srid: 4326); $table->spatialIndex('location'); }) ``` + *** + ### 2- Models: + Fill the `$fillable`, `$casts` arrays in the model: ```php @@ -95,6 +99,7 @@ class Address extends Model { ``` ### 3- Spatial Data Types: + #### ***Point:*** `Point` represents the coordinates of a location and contains `latitude`, `longitude`, and `srid` properties. @@ -116,13 +121,14 @@ $location->getLng(); // 39.123456 $locatipn->getSrid(); // 4326 ``` -You can override the default SRID via the `laravel-spatial` config file. To do that, you should publish the config migration file using vendor:publish artisan command: +You can override the default SRID via the `laravel-spatial` config file. To do that, you should publish the config file using `vendor:publish` artisan command: ```bash php artisan vendor:publish --provider="TarfinLabs\LaravelSpatial\LaravelSpatialServiceProvider" ``` -Then change the value of `default_srid` in `config/laravel-spatial.php` +After that, you can change the value of `default_srid` in `config/laravel-spatial.php` + ```php return [ 'default_srid' => 4326, @@ -130,8 +136,12 @@ return [ ``` *** ### 4- Scopes: + #### ***withinDistanceTo()*** -Filter addresses within 10 km of the given coordinate: + +You can use the `withinDistanceTo()` scope to filter locations by given distance: + +To filter addresses within the range of 10 km from the given coordinate: ```php use TarfinLabs\LaravelSpatial\Types\Point; @@ -143,7 +153,8 @@ Address::query() ``` #### ***selectDistanceTo()*** -Select distance to given coordinates as meter: + +You can get the distance between two points by using `selectDistanceTo()` scope. The distance will be in meters: ```php use TarfinLabs\LaravelSpatial\Types\Point; @@ -155,7 +166,8 @@ Address::query() ``` #### ***orderByDistanceTo()*** -Order data by distance to given coordinates: + +You can order your models by distance to given coordinates: ```php use TarfinLabs\LaravelSpatial\Types\Point; @@ -172,7 +184,7 @@ Address::query() ->get(); ``` -Get latitude and longitude of the location: +#### Get latitude and longitude of the location: ```php use App\Models\Address; @@ -184,7 +196,7 @@ $address->location->getLat(); $address->location->getLng(); ``` -Create a new address with location: +#### Create a new address with location: ```php use App\Models\Address; @@ -202,13 +214,13 @@ Address::create([ composer test ``` -### Todo -- MultiPoint -- LineString -- MultiLineString -- Polygon -- MultiPolygon -- GeometryCollection +### Road Map +- [ ] MultiPoint +- [ ] LineString +- [ ] MultiLineString +- [ ] Polygon +- [ ] MultiPolygon +- [ ] GeometryCollection ### Changelog diff --git a/phpunit.xml b/phpunit.xml index f2eebe0..f05ae55 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,22 +1,13 @@ - - - - tests - - - - - src/ - - + + + + src/ + + + + + tests + + diff --git a/tests/HasSpatialTest.php b/tests/HasSpatialTest.php index 5974875..8b5d6ea 100644 --- a/tests/HasSpatialTest.php +++ b/tests/HasSpatialTest.php @@ -2,13 +2,16 @@ namespace TarfinLabs\LaravelSpatial\Tests; -use Illuminate\Support\Collection; use TarfinLabs\LaravelSpatial\Tests\TestModels\Address; use TarfinLabs\LaravelSpatial\Types\Point; class HasSpatialTest extends TestCase { - public function test_scopeSelectDistanceTo(): void + /** + * @test + * @see + */ + public function it_generates_sql_query_for_selectDistanceTo_scope(): void { // Arrange $address = new Address(); @@ -18,58 +21,85 @@ public function test_scopeSelectDistanceTo(): void $query = $address->selectDistanceTo($castedAttr, new Point()); // Assert - $this->assertEquals("select *, CONCAT(ST_AsText(addresses.{$castedAttr}), ',', ST_SRID(addresses.{$castedAttr})) as {$castedAttr}, ST_Distance( - ST_SRID({$castedAttr}, ?), + $this->assertEquals( + expected: "select *, CONCAT(ST_AsText(addresses.$castedAttr), ',', ST_SRID(addresses.$castedAttr)) as $castedAttr, ST_Distance( + ST_SRID($castedAttr, ?), ST_SRID(Point(?, ?), ?) - ) as distance from `addresses`", $query->toSql()); + ) as distance from `addresses`", + actual: $query->toSql() + ); } - public function test_scopeWithinDistanceTo(): void + /** + * @test + * @see + */ + public function it_generates_sql_query_for_withinDistanceTo_scope(): void { - // Arrange + // 1. Arrange $address = new Address(); $castedAttr = $address->getLocationCastedAttributes()->first(); - // Act + // 2. Act $query = $address->withinDistanceTo($castedAttr, new Point(), 10000); - // Assert - $this->assertEquals("select *, CONCAT(ST_AsText(addresses.{$castedAttr}), ',', ST_SRID(addresses.{$castedAttr})) as {$castedAttr} from `addresses` where ST_Distance( - ST_SRID({$castedAttr}, ?), + // 3. Assert + $this->assertEquals( + expected: "select *, CONCAT(ST_AsText(addresses.$castedAttr), ',', ST_SRID(addresses.$castedAttr)) as $castedAttr from `addresses` where ST_Distance( + ST_SRID($castedAttr, ?), ST_SRID(Point(?, ?), ?) - ) <= ?", $query->toSql()); + ) <= ?", + actual: $query->toSql() + ); } - public function test_scopeOrderByDistanceTo(): void + /** + * @test + * @see + */ + public function it_generates_sql_query_for_orderByDistanceTo_scope(): void { - // Arrange + // 1. Arrange $address = new Address(); $castedAttr = $address->getLocationCastedAttributes()->first(); - // Act + // 2. Act $queryForAsc = $address->orderByDistanceTo($castedAttr, new Point()); $queryForDesc = $address->orderByDistanceTo($castedAttr, new Point(), 'desc'); - // Assert - $this->assertEquals("select *, CONCAT(ST_AsText(addresses.{$castedAttr}), ',', ST_SRID(addresses.{$castedAttr})) as {$castedAttr} from `addresses` order by ST_Distance( - ST_SRID({$castedAttr}, ?), + // 3. Assert + $this->assertEquals( + expected: "select *, CONCAT(ST_AsText(addresses.$castedAttr), ',', ST_SRID(addresses.$castedAttr)) as $castedAttr from `addresses` order by ST_Distance( + ST_SRID($castedAttr, ?), ST_SRID(Point(?, ?), ?) - ) asc", $queryForAsc->toSql()); + ) asc", + actual: $queryForAsc->toSql() + ); - $this->assertEquals("select *, CONCAT(ST_AsText(addresses.{$castedAttr}), ',', ST_SRID(addresses.{$castedAttr})) as {$castedAttr} from `addresses` order by ST_Distance( - ST_SRID({$castedAttr}, ?), + $this->assertEquals( + expected: "select *, CONCAT(ST_AsText(addresses.$castedAttr), ',', ST_SRID(addresses.$castedAttr)) as $castedAttr from `addresses` order by ST_Distance( + ST_SRID($castedAttr, ?), ST_SRID(Point(?, ?), ?) - ) desc", $queryForDesc->toSql()); + ) desc", + actual: $queryForDesc->toSql() + ); } - public function test_newQuery(): void + /** + * @test + * @see + */ + public function it_generates_sql_query_for_location_casted_attributes(): void { - // Arrange + // 1. Arrange $address = new Address(); $castedAttr = $address->getLocationCastedAttributes()->first(); - // Assert - $this->assertEquals("select *, CONCAT(ST_AsText(addresses.{$castedAttr}), ',', ST_SRID(addresses.{$castedAttr})) as {$castedAttr} from `addresses`", $address->query()->toSql()); + // 2. Act & Assert + $this->assertEquals( + expected: "select *, CONCAT(ST_AsText(addresses.$castedAttr), ',', ST_SRID(addresses.$castedAttr)) as $castedAttr from `addresses`", + actual: $address->query()->toSql() + ); } /** @@ -78,14 +108,13 @@ public function test_newQuery(): void */ public function it_returns_location_casted_attributes(): void { - // Arrange + // 1. Arrange $address = new Address(); - // Act + // 2. Act $locationCastedAttributres = $address->getLocationCastedAttributes(); - // Assert - $this->assertInstanceOf(Collection::class, $locationCastedAttributres); + // 3. Assert $this->assertEquals(collect(['location']), $locationCastedAttributres); } } diff --git a/tests/LocationCastTest.php b/tests/LocationCastTest.php index 0195bb4..4861a7c 100644 --- a/tests/LocationCastTest.php +++ b/tests/LocationCastTest.php @@ -10,57 +10,65 @@ class LocationCastTest extends TestCase { - public function test_setting_location_to_a_non_point_value(): void + /** @test */ + public function it_throws_an_exception_if_casted_attribute_set_to_a_non_point_value(): void { - // Arrange + // 1. Arrange $address = new Address(); + // 3. Expect $this->expectException(Exception::class); - // Act + // 2. Act $address->location = 'dummy'; } - public function test_setting_location_to_a_point(): void + /** @test */ + public function it_can_set_the_casted_attribute_to_a_point(): void { + // 1. Arrange $address = new Address(); $point = new Point(27.1234, 39.1234); $cast = new LocationCast(); + + // 2. Act $response = $cast->set($address, 'location', $point, $address->getAttributes()); - // Assert + // 3. Assert $this->assertEquals(DB::raw("ST_GeomFromText('POINT({$point->getLng()} {$point->getLat()})')"), $response); } - public function test_getting_location(): void + /** @test */ + public function it_can_get_a_casted_attribute(): void { - // Arrange + // 1. Arrange $address = new Address(); $point = new Point(27.1234, 39.1234); - // Act + // 2. Act $address->location = $point; $address->save(); - // Assert + // 3. Assert $this->assertInstanceOf(Point::class, $address->location); $this->assertEquals($point->getLat(), $address->location->getLat()); $this->assertEquals($point->getLng(), $address->location->getLng()); $this->assertEquals($point->getSrid(), $address->location->getSrid()); } - public function test_serialize_location(): void + /** @test */ + public function it_can_serialize_a_casted_attribute(): void { - // Arrange + // 1. Arrange $address = new Address(); $point = new Point(27.1234, 39.1234); - // Act + // 2. Act $address->location = $point; $address->save(); - // Assert + // 3. Assert $array = $address->toArray(); $this->assertIsArray($array); $this->assertArrayHasKey('location', $array); diff --git a/tests/PointTest.php b/tests/PointTest.php index e61d773..2579c78 100644 --- a/tests/PointTest.php +++ b/tests/PointTest.php @@ -12,27 +12,27 @@ class PointTest extends TestCase /** @test */ public function it_sets_lat_lng_and_srid_in_constructor(): void { - // Arrange + // 1. Arrange $lat = 25.1515; $lng = 36.1212; $srid = 4326; - // Act + // 2. Act $point = new Point(lat: $lat, lng: $lng, srid: $srid); - // Assert + // 3. Assert $this->assertSame(expected: $lat, actual: $point->getLat()); $this->assertSame(expected: $lng, actual: $point->getLng()); $this->assertSame(expected: $srid, actual: $point->getSrid()); } /** @test */ - public function it_returns_default_lat_lng_and_srid_if_they_are_not_given_to_constructor(): void + public function it_returns_default_lat_lng_and_srid_if_they_are_not_given_in_the_constructor(): void { - // Act + // 1. Act $point = new Point(); - // Assert + // 2. Assert $this->assertSame(expected: 0.0, actual: $point->getLat()); $this->assertSame(expected: 0.0, actual: $point->getLng()); $this->assertSame(expected: 0, actual: $point->getSrid()); @@ -41,13 +41,13 @@ public function it_returns_default_lat_lng_and_srid_if_they_are_not_given_to_con /** @test */ public function it_returns_default_srid_in_config_if_it_is_not_null(): void { - // Arrange + // 1. Arrange Config::set('laravel-spatial.default_srid', 4326); - // Act + // 2. Act $point = new Point(); - // Assert + // 3. Assert $this->assertSame(expected: 0.0, actual: $point->getLat()); $this->assertSame(expected: 0.0, actual: $point->getLng()); $this->assertSame(expected: 4326, actual: $point->getSrid()); diff --git a/tests/TestModels/Address.php b/tests/TestModels/Address.php index 6dbd6af..0312bca 100644 --- a/tests/TestModels/Address.php +++ b/tests/TestModels/Address.php @@ -4,10 +4,21 @@ namespace TarfinLabs\LaravelSpatial\Tests\TestModels; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use TarfinLabs\LaravelSpatial\Casts\LocationCast; use TarfinLabs\LaravelSpatial\Traits\HasSpatial; +use TarfinLabs\LaravelSpatial\Types\Point; +/** + * Class Address + * + * @method void selectDistanceTo(Builder $query, string $column, Point $point) + * @method void orderByDistanceTo(Builder $query, string $column, Point $point, string $direction = 'asc') + * @method void withinDistanceTo(Builder $query, string $column, Point $point, int $distance) + * + * @property Point location + */ class Address extends Model { use HasSpatial;