Skip to content

Commit

Permalink
Merge pull request #120 from clickbar/geometry-cast-expressions
Browse files Browse the repository at this point in the history
[2.x] Refactor to remove GeometryType enum
  • Loading branch information
saibotk authored Dec 31, 2024
2 parents 4bcd4cf + 024fb76 commit 6eb5a6a
Show file tree
Hide file tree
Showing 27 changed files with 499 additions and 392 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Removed `GeometryWKBCast`
- Removed automatic SRID transformation
- Removed st prefixed builder functions (e.g. `stSelect`, `stWhere`, ...)
- Removed `GeometryType` Enum

### Added

- Added `Castable` to all geometries to use them as casters, instead of the `GeometryWKBCast`
- Added `Aliased` Expression class as wrapper for `AS` in query selects
- Added `withMagellanCasts()` as EloquentBuilder macro
- Added `AsGeometry` and `AsGeography` database expressions

### Improved

- Validate the structure of Geometry coordinates to be an array in the `GeojsonParser` and fail if not
- Use of ST functions directly in the Laravel default builder methods
- Use of ST functions directly in Model::create array
- Renamed parameters of ST functions that can receive geometry or geography from `$geometry` to `$geometryOrGeography`
- Geometry & Box implements `Expression` and therefore can be used in `->select(...)` directly now

### Removed

Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,23 @@ Since we use Laravel Database Expressions for a seamless integration into the de
//--> leads to SELECT ST_DistanceSphere(<<currentShipPosition, 'location') AS distance_to_ship
```

### Geometry or Geography
Using PostGIS, you will encounter those two types of geometries. Most of the functions in PostGIS are only defined with parameters of the type `Geometry`. But sometimes you explicitly want to add casts to your parameters. Therefore, we added two cast expressions:
- `Geometry` => `\Clickbar\Magellan\Database\Expressions\AsGeometry`
- `Geography` => `\Clickbar\Magellan\Database\Expressions\AsGeography`

Considering we want to buffer the location of our ports by 50 meters. Looking into the PostGIS documentation we can see the following:
> For geometry, the distance is specified in the units of the Spatial Reference System of the geometry. For geography, the distance is specified in meters.
> [https://postgis.net/docs/ST_Buffer.html](https://postgis.net/docs/ST_Buffer.html)

Therefore, we need to cast our points from the location colum to geography before handing them over to the buffer function:
```php
$bufferedPorts = Port::query()
->select(new Aliased(ST::buffer(new AsGeography('location'), 50), alias: 'buffered_location'))
->withCasts(['buffered_location' => Polygon::class])
->get();
```



### Autocast for BBox or geometries
Expand Down
6 changes: 2 additions & 4 deletions src/Data/Boxes/Box.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@

namespace Clickbar\Magellan\Data\Boxes;

use Illuminate\Database\Query\Expression;
use Illuminate\Contracts\Database\Query\Expression as ExpressionContract;

abstract class Box
abstract class Box implements ExpressionContract
{
abstract public function toString(): string;

abstract public function toExpression(): Expression;
}
6 changes: 3 additions & 3 deletions src/Data/Boxes/Box2D.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Clickbar\Magellan\Data\Boxes;

use Illuminate\Database\Query\Expression;
use Illuminate\Database\Grammar;

class Box2D extends Box
{
Expand Down Expand Up @@ -40,8 +40,8 @@ public function toString(): string
return "BOX({$this->xMin} {$this->yMin},{$this->xMax} {$this->yMax})";
}

public function toExpression(): Expression
public function getValue(Grammar $grammar): string
{
return new Expression("'{$this->toString()}'::box2d");
return $grammar->quoteString($this->toString()).'::box2d';
}
}
6 changes: 3 additions & 3 deletions src/Data/Boxes/Box3D.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Clickbar\Magellan\Data\Boxes;

use Illuminate\Database\Query\Expression;
use Illuminate\Database\Grammar;

class Box3D extends Box
{
Expand Down Expand Up @@ -50,8 +50,8 @@ public function toString(): string
return "BOX3D({$this->xMin} {$this->yMin} {$this->zMin},{$this->xMax} {$this->yMax} {$this->zMax})";
}

public function toExpression(): Expression
public function getValue(Grammar $grammar): string
{
return new Expression("'{$this->toString()}'::box3d");
return $grammar->quoteString($this->toString()).'::box3d';
}
}
13 changes: 12 additions & 1 deletion src/Data/Geometries/Geometry.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
namespace Clickbar\Magellan\Data\Geometries;

use Clickbar\Magellan\Cast\GeometryCast;
use Clickbar\Magellan\IO\Generator\WKT\WKTGenerator;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Query\Expression;
use Illuminate\Database\Grammar;
use Illuminate\Support\Facades\Config;
use JsonSerializable;

abstract class Geometry implements \Stringable, Castable, GeometryInterface, JsonSerializable
abstract class Geometry implements \Stringable, Castable, Expression, GeometryInterface, JsonSerializable
{
public function __construct(
protected ?int $srid = null,
Expand Down Expand Up @@ -60,4 +63,12 @@ public static function castUsing(array $arguments): GeometryCast

return new GeometryCast($class);
}

public function getValue(Grammar $grammar): string
{
$generatorClass = Config::get('magellan.sql_generator', WKTGenerator::class);
$generator = new $generatorClass();

return $generator->toPostgisGeometrySql($this, Config::get('magellan.schema'));
}
}
62 changes: 62 additions & 0 deletions src/Database/Builder/StringifiesQueryParameters.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace Clickbar\Magellan\Database\Builder;

use Clickbar\Magellan\Database\MagellanExpressions\ColumnParameter;
use Closure;
use Illuminate\Contracts\Database\Query\Expression;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Grammar;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;

trait StringifiesQueryParameters
{
public function stringifyQueryParameter(Grammar $grammar, mixed $param): string
{

// 1. Check if param is queryable -> it's a subquery
if ($this->isQueryable($param)) {
// --> Create sub and replace with param array
return $this->createSub($param);
}

// 2. Basic Binding Value
if ($param instanceof ValueParameter) {
// --> escape and replace
return $grammar->escape($param->getValue());
}

// 3. expression
if ($param instanceof Expression) {
return $param->getValue($grammar);
}

// 4. Column Parameter
if ($param instanceof ColumnParameter) {
return $grammar->wrap($param->getValue());
}

// 5. string / default
return $grammar->wrap($param);
}

private function createSub($query): string
{
if ($query instanceof Closure) {
$callback = $query;
$callback($query = DB::query());
}

return "({$query->toRawSql()})";
}

private function isQueryable($value): bool
{
return $value instanceof Builder ||
$value instanceof EloquentBuilder ||
$value instanceof Relation ||
$value instanceof Closure;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Clickbar\Magellan\Database\Builder;

class BindingExpression
class ValueParameter
{
public function __construct(protected mixed $value)
{
Expand Down
22 changes: 22 additions & 0 deletions src/Database/Expressions/AsGeography.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Clickbar\Magellan\Database\Expressions;

use Closure;
use Illuminate\Contracts\Database\Query\Expression as ExpressionContract;
use Illuminate\Database\Grammar;

class AsGeography extends GeometryWrapperExpression
{
public function __construct(
public string|ExpressionContract|Closure $expression,
) {
}

public function getValue(Grammar $grammar): string
{
$expression = $this->stringifyQueryParameter($grammar, $this->expression);

return "($expression)::geography";
}
}
22 changes: 22 additions & 0 deletions src/Database/Expressions/AsGeometry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Clickbar\Magellan\Database\Expressions;

use Closure;
use Illuminate\Contracts\Database\Query\Expression as ExpressionContract;
use Illuminate\Database\Grammar;

class AsGeometry extends GeometryWrapperExpression
{
public function __construct(
public string|ExpressionContract|Closure $expression,
) {
}

public function getValue(Grammar $grammar): string
{
$expression = $this->stringifyQueryParameter($grammar, $this->expression);

return "($expression)::geometry";
}
}
11 changes: 11 additions & 0 deletions src/Database/Expressions/GeometryWrapperExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Clickbar\Magellan\Database\Expressions;

use Clickbar\Magellan\Database\Builder\StringifiesQueryParameters;
use Illuminate\Contracts\Database\Query\Expression;

abstract class GeometryWrapperExpression implements Expression
{
use StringifiesQueryParameters;
}
36 changes: 36 additions & 0 deletions src/Database/MagellanExpressions/ColumnParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Clickbar\Magellan\Database\MagellanExpressions;

/**
* @internal
*
* Wrapper class for the parameters that represents a database column.
* We need this wrapper to distinguish when generating the actual SQL of a MagellanBaseExpression.
*/
class ColumnParameter
{
protected function __construct(
protected readonly string $column,
) {
}

/**
* Only wrap the given value into the ColumnParameter if it is string.
*
* @return self|mixed
*/
public static function wrap(mixed $value): mixed
{
if (is_string($value)) {
return new self($value);
}

return $value;
}

public function getValue(): string
{
return $this->column;
}
}
26 changes: 0 additions & 26 deletions src/Database/MagellanExpressions/GeoParam.php

This file was deleted.

Loading

0 comments on commit 6eb5a6a

Please sign in to comment.