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
8 changes: 8 additions & 0 deletions .run/Fix formatting.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Fix formatting" type="ComposerRunConfigurationType" factoryName="Composer Script">
<option name="commandLineParameters" value="" />
<option name="pathToComposerJson" value="$PROJECT_DIR$/composer.json" />
<option name="script" value="php-cs-fixer" />
<method v="2" />
</configuration>
</component>
8 changes: 8 additions & 0 deletions .run/Static code analysis.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Static code analysis" type="ComposerRunConfigurationType" factoryName="Composer Script">
<option name="commandLineParameters" value="" />
<option name="pathToComposerJson" value="$PROJECT_DIR$/composer.json" />
<option name="script" value="phpstan" />
<method v="2" />
</configuration>
</component>
11 changes: 11 additions & 0 deletions .run/Test.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Test" type="PestRunConfigurationType">
<option name="pestRunnerSettings">
<PestRunner method="" scope="ConfigurationFile" />
</option>
<option name="runnerSettings">
<PhpTestRunnerSettings method="" scope="ConfigurationFile" />
</option>
<method v="2" />
</configuration>
</component>
51 changes: 39 additions & 12 deletions API.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
# API

## Available spatial classes
## Available geometry classes

* `Point(float $latitude, float $longitude)` - [MySQL Point](https://dev.mysql.com/doc/refman/8.0/en/gis-class-point.html)
* `MultiPoint(Point[] | Collection<Point>)` - [MySQL MultiPoint](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multipoint.html)
* `LineString(Point[] | Collection<Point>)` - [MySQL LineString](https://dev.mysql.com/doc/refman/8.0/en/gis-class-linestring.html)
* `MultiLineString(LineString[] | Collection<LineString>)` - [MySQL MultiLineString](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multilinestring.html)
* `Polygon(LineString[] | Collection<LineString>)` - [MySQL Polygon](https://dev.mysql.com/doc/refman/8.0/en/gis-class-polygon.html)
* `MultiPolygon(Polygon[] | Collection<Polygon>)` - [MySQL MultiPolygon](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multipolygon.html)
* `GeometryCollection(Geometry[] | Collection<Geometry>)` - [MySQL GeometryCollection](https://dev.mysql.com/doc/refman/8.0/en/gis-class-geometrycollection.html)
* `Point(float $latitude, float $longitude, int $srid = 0)` - [MySQL Point](https://dev.mysql.com/doc/refman/8.0/en/gis-class-point.html)
* `MultiPoint(Point[] | Collection<Point>, int $srid = 0)` - [MySQL MultiPoint](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multipoint.html)
* `LineString(Point[] | Collection<Point>, int $srid = 0)` - [MySQL LineString](https://dev.mysql.com/doc/refman/8.0/en/gis-class-linestring.html)
* `MultiLineString(LineString[] | Collection<LineString>, int $srid = 0)` - [MySQL MultiLineString](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multilinestring.html)
* `Polygon(LineString[] | Collection<LineString>, int $srid = 0)` - [MySQL Polygon](https://dev.mysql.com/doc/refman/8.0/en/gis-class-polygon.html)
* `MultiPolygon(Polygon[] | Collection<Polygon>, int $srid = 0)` - [MySQL MultiPolygon](https://dev.mysql.com/doc/refman/8.0/en/gis-class-multipolygon.html)
* `GeometryCollection(Geometry[] | Collection<Geometry>, int $srid = 0)` - [MySQL GeometryCollection](https://dev.mysql.com/doc/refman/8.0/en/gis-class-geometrycollection.html)

## Available spatial functions
Geometry classes can be also created by these static methods:

Every geometry class has these functions:
* `fromJson(string $geoJson, int $srid = 0)` - Creates a geometry object from a [GeoJSON](https://en.wikipedia.org/wiki/GeoJSON) string.
* `fromWkt(string $wkt, int $srid = 0)` - Creates a geometry object from a [WKT](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry).
* `fromWkb(string $wkb, int $srid = 0)` - Creates a geometry object from a [WKB](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Well-known_binary).

## Available geometry class methods

* `toArray()` - Serializes the geometry object into a GeoJSON associative array.
* `toJson()` - Serializes the geometry object into an GeoJSON string.
* `fromJson(string $geoJson)` - Deserializes a geometry object from a GeoJSON string. (static method)
* `toFeatureCollectionJson()` - Serializes the geometry object into an GeoJSON's FeatureCollection string.
* `toWkt()` - Serializes the geometry object into a WKT.
* `toWkb()` - Serializes the geometry object into a WKB.
* `getCoordinates()` - Returns the coordinates of the geometry object.

In addition, `GeometryCollection` also has these functions:
Expand All @@ -40,7 +45,7 @@ $geometryCollection = new GeometryCollection([
]);

echo $geometryCollection->getGeometries()[1]->latitude; // 180
// can also access as an array:
// or access as an array:
echo $geometryCollection[1]->latitude; // 180
```

Expand All @@ -59,6 +64,7 @@ echo $geometryCollection[1]->latitude; // 180
* [whereCrosses](#whereCrosses)
* [whereDisjoint](#whereDisjoint)
* [whereEquals](#whereEquals)
* [whereSrid](#whereSrid)

### withDistance

Expand Down Expand Up @@ -372,3 +378,24 @@ Place::query()
```
</details>

### whereSrid

Filters records by the [ST_Srid](https://dev.mysql.com/doc/refman/8.0/en/gis-general-property-functions.html#function_st-srid) function.

| parameter name | type
| ------------------ | --------------------
| `$column` | `string`
| `$operator` | `string`
| `$value` | `int`

<details><summary>Example</summary>

```php
Place::create(['location' => new Point(0, 0, 4326)]);

Place::query()
->whereSrid('location', '=', 4326)
->exists(); // true
```
</details>

26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@ use MatanYadaev\EloquentSpatial\Objects\Point;

$londonEye = Place::create([
'name' => 'London Eye',
'location' => new Point(51.5032973, -0.1195537)
'location' => new Point(51.5032973, -0.1217424),
]);

$whiteHouse = Place::create([
'name' => 'White House',
'location' => new Point(38.8976763, -77.0365298, 4326), // with SRID
]);

$vaticanCity = Place::create([
Expand All @@ -119,16 +124,18 @@ $vaticanCity = Place::create([
new Point(12.457734346389769, 41.905918239316286),
new Point(12.45572805404663, 41.90637337450963),
new Point(12.455363273620605, 41.90746728266806),
])
])
]),
]),
])
```

Retrieve a record with spatial data:

```php
echo $londonEye->location->latitude; // 51.5032973
echo $londonEye->location->longitude; // -0.1195537
echo $londonEye->location->longitude; // -0.1217424

echo $whiteHouse->location->srid; // 4326

echo $vacationCity->area->toJson(); // {"type":"Polygon","coordinates":[[[41.90746728266806,12.455363273620605],[41.906636872349075,12.450309991836548],[41.90197359839437,12.445632219314575],[41.90027269624499,12.447413206100464],[41.90000118654431,12.457906007766724],[41.90281205461268,12.458517551422117],[41.903107507989986,12.457584142684937],[41.905918239316286,12.457734346389769],[41.90637337450963,12.45572805404663],[41.90746728266806,12.455363273620605]]]}
```
Expand Down Expand Up @@ -170,13 +177,12 @@ Place::query()->whereDistance(...); // This is IDE-friendly
Place::whereDistance(...); // This is not
```

## Tests
## Development

``` bash
composer phpunit
# or with coverage
composer phpunit-coverage
```
* Test: `composer pest`
* Test with coverage: `composer pest-coverage`
* Type check: `composer phpstan`
* Format: `composer php-cs-fixer`

## Changelog

Expand Down
27 changes: 11 additions & 16 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,8 @@

class Factory
{
public static function parse(string $value, bool $isWkb): Geometry
public static function parse(string $value): Geometry
{
if ($isWkb) {
// MySQL adds 4 NULL bytes at the start of the WKB
$value = substr($value, 4);
}

try {
/** @var geoPHPGeometry|false $geoPHPGeometry */
$geoPHPGeometry = geoPHP::load($value);
Expand All @@ -46,14 +41,14 @@ public static function parse(string $value, bool $isWkb): Geometry

protected static function createFromGeometry(geoPHPGeometry $geometry): Geometry
{
$srid = is_int($geometry->getSRID()) ? $geometry->getSRID() : 0;

if ($geometry instanceof geoPHPPoint) {
if ($geometry->coords[0] === null || $geometry->coords[1] === null) {
if (! isset($geoPHPGeometry) || ! $geoPHPGeometry) {
throw new InvalidArgumentException('Invalid spatial value');
}
throw new InvalidArgumentException('Invalid spatial value');
}

return new Point($geometry->coords[1], $geometry->coords[0]);
return new Point($geometry->coords[1], $geometry->coords[0], $srid);
}

/** @var geoPHPGeometryCollection $geometry */
Expand All @@ -63,25 +58,25 @@ protected static function createFromGeometry(geoPHPGeometry $geometry): Geometry
});

if ($geometry::class === geoPHPMultiPoint::class) {
return new MultiPoint($components);
return new MultiPoint($components, $srid);
}

if ($geometry::class === geoPHPLineString::class) {
return new LineString($components);
return new LineString($components, $srid);
}

if ($geometry::class === geoPHPPolygon::class) {
return new Polygon($components);
return new Polygon($components, $srid);
}

if ($geometry::class === geoPHPMultiLineString::class) {
return new MultiLineString($components);
return new MultiLineString($components, $srid);
}

if ($geometry::class === geoPHPMultiPolygon::class) {
return new MultiPolygon($components);
return new MultiPolygon($components, $srid);
}

return new GeometryCollection($components);
return new GeometryCollection($components, $srid);
}
}
38 changes: 23 additions & 15 deletions src/GeometryCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,56 +27,64 @@ public function __construct(string $className)
/**
* @param Model $model
* @param string $key
* @param string|Expression|null $wkbOrWKt
* @param string|Expression|null $value
* @param array<string, mixed> $attributes
* @return Geometry|null
*/
public function get($model, string $key, $wkbOrWKt, array $attributes): ?Geometry
public function get($model, string $key, $value, array $attributes): ?Geometry
{
if (! $wkbOrWKt) {
if (! $value) {
return null;
}

if ($wkbOrWKt instanceof Expression) {
$wkt = $this->extractWktFromExpression($wkbOrWKt);
if ($value instanceof Expression) {
$wkt = $this->extractWktFromExpression($value);
$srid = $this->extractSridFromExpression($value);

return $this->className::fromWkt($wkt);
return $this->className::fromWkt($wkt, $srid);
}

return $this->className::fromWkb($wkbOrWKt);
return $this->className::fromWkb($value);
}

/**
* @param Model $model
* @param string $key
* @param Geometry|mixed|null $geometry
* @param Geometry|mixed|null $value
* @param array<string, mixed> $attributes
* @return Expression|null
*
* @throws InvalidArgumentException
*/
public function set($model, string $key, $geometry, array $attributes): Expression|null
public function set($model, string $key, $value, array $attributes): Expression|null
{
if (! $geometry) {
if (! $value) {
return null;
}

if (! ($geometry instanceof $this->className)) {
$geometryType = is_object($geometry) ? $geometry::class : gettype($geometry);
if (! ($value instanceof $this->className)) {
$geometryType = is_object($value) ? $value::class : gettype($value);
throw new InvalidArgumentException(
sprintf('Expected %s, %s given.', static::class, $geometryType)
);
}

$wkt = $geometry->toWkt(withFunction: true);
$wkt = $value->toWkt();

return DB::raw("ST_GeomFromText('{$wkt}')");
return DB::raw("ST_GeomFromText('{$wkt}', {$value->srid})");
}

private function extractWktFromExpression(Expression $expression): string
{
preg_match('/ST_GeomFromText\(\'(.+)\'\)/', (string) $expression, $match);
preg_match('/ST_GeomFromText\(\'(.+)\', .+\)/', (string) $expression, $match);

return $match[1];
}

private function extractSridFromExpression(Expression $expression): int
{
preg_match('/ST_GeomFromText\(\'.+\', (.+)\)/', (string) $expression, $match);

return (int) $match[1];
}
}
31 changes: 24 additions & 7 deletions src/Objects/Geometry.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

abstract class Geometry implements Castable, Arrayable, Jsonable, JsonSerializable
{
public int $srid = 0;

abstract public function toWkt(bool $withFunction = true): string;

/**
Expand All @@ -33,10 +35,14 @@ public function toJson($options = 0): string

public function toWkb(): string
{
$geoPHPGeometry = geoPHP::load($this->toWkt());
$geoPHPGeometry = geoPHP::load($this->toJson());

$sridInBinary = pack('L', $this->srid);

// @phpstan-ignore-next-line
return (new geoPHPWkb)->write($geoPHPGeometry, true);
$wkbWithoutSrid = (new geoPHPWkb)->write($geoPHPGeometry);

return $sridInBinary.$wkbWithoutSrid;
}

/**
Expand All @@ -47,7 +53,14 @@ public function toWkb(): string
*/
public static function fromWkb(string $wkb): static
{
$geometry = Factory::parse($wkb, true);
$srid = substr($wkb, 0, 4);
// @phpstan-ignore-next-line
$srid = unpack('L', $srid)[1];

$wkb = substr($wkb, 4);

$geometry = Factory::parse($wkb);
$geometry->srid = $srid;

if (! ($geometry instanceof static)) {
throw new InvalidArgumentException(
Expand All @@ -60,13 +73,15 @@ public static function fromWkb(string $wkb): static

/**
* @param string $wkt
* @param int $srid
* @return static
*
* @throws InvalidArgumentException
*/
public static function fromWkt(string $wkt): static
public static function fromWkt(string $wkt, int $srid = 0): static
{
$geometry = Factory::parse($wkt, false);
$geometry = Factory::parse($wkt);
$geometry->srid = $srid;

if (! ($geometry instanceof static)) {
throw new InvalidArgumentException(
Expand All @@ -79,13 +94,15 @@ public static function fromWkt(string $wkt): static

/**
* @param string $geoJson
* @param int $srid
* @return static
*
* @throws InvalidArgumentException
*/
public static function fromJson(string $geoJson): static
public static function fromJson(string $geoJson, int $srid = 0): static
{
$geometry = Factory::parse($geoJson, false);
$geometry = Factory::parse($geoJson);
$geometry->srid = $srid;

if (! ($geometry instanceof static)) {
throw new InvalidArgumentException(
Expand Down
Loading