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
48 changes: 30 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -116,22 +121,27 @@ $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,
];
```
***
### 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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -202,13 +214,13 @@ Address::create([
composer test
```

### Todo
- MultiPoint
- LineString
- MultiLineString
- Polygon
- MultiPolygon
- GeometryCollection
### Road Map
- [ ] MultiPoint
- [ ] LineString
- [ ] MultiLineString
- [ ] Polygon
- [ ] MultiPolygon
- [ ] GeometryCollection

### Changelog

Expand Down
31 changes: 11 additions & 20 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
verbose="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">src/</directory>
</whitelist>
</filter>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" backupGlobals="false" backupStaticAttributes="false" colors="true" verbose="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<coverage>
<include>
<directory suffix=".php">src/</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
89 changes: 59 additions & 30 deletions tests/HasSpatialTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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()
);
}

/**
Expand All @@ -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);
}
}
34 changes: 21 additions & 13 deletions tests/LocationCastTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading