diff --git a/README.md b/README.md index 9c3da68..5754958 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,52 @@ Place::query()->whereDistance(...); // This is IDE-friendly Place::whereDistance(...); // This is not ``` +## Extension + +You can extend the package by creating your own geometry classes. + +1. Create an extended geometry class: + ```php + class ExtendedPoint extends Point + { + public function toCustomArray(): array + { + return [ + 'latitude' => $this->latitude, + 'longitude' => $this->longitude, + ]; + } + } + ``` +2. Update the geometry class mapping in a service provider file. + ```php + class AppServiceProvider extends ServiceProvider + { + public function boot(): void + { + LaravelEloquentSpatial::$pointClass = ExtendedPoint::class; + } + } + ``` +3. Cast the extended geometry class. + ```php + class Place extends Model + { + protected $casts = [ + 'location' => ExtendedPoint::class, + ]; + } + ``` +4. Retrieve a record with the extended geometry class. + ```php + $place = Place::create([ + 'name' => 'London Eye', + 'location' => new ExtendedPoint(51.5032973, -0.1217424), + ]); + + $place->fresh()->location->toCustomArray(); // ['latitude' => 51.5032973, 'longitude' => -0.1217424] + ``` + ## Development * Test: `composer pest` diff --git a/src/Factory.php b/src/Factory.php index f946a03..c3a6217 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -10,13 +10,7 @@ use InvalidArgumentException; use LineString as geoPHPLineString; use MatanYadaev\EloquentSpatial\Objects\Geometry; -use MatanYadaev\EloquentSpatial\Objects\GeometryCollection; -use MatanYadaev\EloquentSpatial\Objects\LineString; -use MatanYadaev\EloquentSpatial\Objects\MultiLineString; -use MatanYadaev\EloquentSpatial\Objects\MultiPoint; -use MatanYadaev\EloquentSpatial\Objects\MultiPolygon; -use MatanYadaev\EloquentSpatial\Objects\Point; -use MatanYadaev\EloquentSpatial\Objects\Polygon; +use MatanYadaev\EloquentSpatial\Tests\LaravelEloquentSpatial; use MultiLineString as geoPHPMultiLineString; use MultiPoint as geoPHPMultiPoint; use MultiPolygon as geoPHPMultiPolygon; @@ -48,7 +42,7 @@ protected static function createFromGeometry(geoPHPGeometry $geometry): Geometry throw new InvalidArgumentException('Invalid spatial value'); } - return new Point($geometry->coords[1], $geometry->coords[0], $srid); + return new LaravelEloquentSpatial::$pointClass($geometry->coords[1], $geometry->coords[0], $srid); } /** @var geoPHPGeometryCollection $geometry */ @@ -58,25 +52,25 @@ protected static function createFromGeometry(geoPHPGeometry $geometry): Geometry }); if ($geometry::class === geoPHPMultiPoint::class) { - return new MultiPoint($components, $srid); + return new LaravelEloquentSpatial::$multiPointClass($components, $srid); } if ($geometry::class === geoPHPLineString::class) { - return new LineString($components, $srid); + return new LaravelEloquentSpatial::$lineStringClass($components, $srid); } if ($geometry::class === geoPHPPolygon::class) { - return new Polygon($components, $srid); + return new LaravelEloquentSpatial::$polygonClass($components, $srid); } if ($geometry::class === geoPHPMultiLineString::class) { - return new MultiLineString($components, $srid); + return new LaravelEloquentSpatial::$multiLineStringClass($components, $srid); } if ($geometry::class === geoPHPMultiPolygon::class) { - return new MultiPolygon($components, $srid); + return new LaravelEloquentSpatial::$multiPolygonClass($components, $srid); } - return new GeometryCollection($components, $srid); + return new LaravelEloquentSpatial::$geometryCollectionClass($components, $srid); } } diff --git a/src/GeometryCast.php b/src/GeometryCast.php index 4339c92..8b24750 100644 --- a/src/GeometryCast.php +++ b/src/GeometryCast.php @@ -65,7 +65,7 @@ public function set($model, string $key, $value, array $attributes): Expression| if (! ($value instanceof $this->className)) { $geometryType = is_object($value) ? $value::class : gettype($value); throw new InvalidArgumentException( - sprintf('Expected %s, %s given.', static::class, $geometryType) + sprintf('Expected %s, %s given.', $this->className, $geometryType) ); } diff --git a/tests/LaravelEloquentSpatial.php b/tests/LaravelEloquentSpatial.php new file mode 100644 index 0000000..dbd7372 --- /dev/null +++ b/tests/LaravelEloquentSpatial.php @@ -0,0 +1,35 @@ + */ + public static string $pointClass = Point::class; + + /** @var class-string */ + public static string $lineStringClass = LineString::class; + + /** @var class-string */ + public static string $multiPointClass = MultiPoint::class; + + /** @var class-string */ + public static string $polygonClass = Polygon::class; + + /** @var class-string */ + public static string $multiLineStringClass = MultiLineString::class; + + /** @var class-string */ + public static string $multiPolygonClass = MultiPolygon::class; + + /** @var class-string */ + public static string $geometryCollectionClass = GeometryCollection::class; +} diff --git a/tests/Objects/GeometryCollectionTest.php b/tests/Objects/GeometryCollectionTest.php index 3f51ba6..d8f8402 100644 --- a/tests/Objects/GeometryCollectionTest.php +++ b/tests/Objects/GeometryCollectionTest.php @@ -5,7 +5,10 @@ use MatanYadaev\EloquentSpatial\Objects\LineString; use MatanYadaev\EloquentSpatial\Objects\Point; use MatanYadaev\EloquentSpatial\Objects\Polygon; +use MatanYadaev\EloquentSpatial\Tests\LaravelEloquentSpatial; +use MatanYadaev\EloquentSpatial\Tests\TestModels\TestExtendedPlace; use MatanYadaev\EloquentSpatial\Tests\TestModels\TestPlace; +use MatanYadaev\EloquentSpatial\Tests\TestObjects\ExtendedGeometryCollection; uses(DatabaseMigrations::class); @@ -365,3 +368,47 @@ expect($geometryCollection->__toString())->toEqual('GEOMETRYCOLLECTION(POLYGON((180 0, 179 1, 178 2, 177 3, 180 0)), POINT(180 0))'); }); + +it('uses an extended GeometryCollection class', function (): void { + LaravelEloquentSpatial::$geometryCollectionClass = ExtendedGeometryCollection::class; + + $geometryCollection = new ExtendedGeometryCollection([ + new Polygon([ + new LineString([ + new Point(0, 180), + new Point(1, 179), + new Point(2, 178), + new Point(3, 177), + new Point(0, 180), + ]), + ]), + new Point(0, 180), + ], 4326); + + /** @var TestExtendedPlace $testPlace */ + $testPlace = TestExtendedPlace::factory()->create(['geometry_collection' => $geometryCollection])->fresh(); + + expect($testPlace->geometry_collection)->toBeInstanceOf(ExtendedGeometryCollection::class); + expect($testPlace->geometry_collection)->toEqual($geometryCollection); +}); + +it('throws exception when storing a record with regular GeometryCollection instead of the extended one', function (): void { + LaravelEloquentSpatial::$geometryCollectionClass = ExtendedGeometryCollection::class; + + $geometryCollection = new GeometryCollection([ + new Polygon([ + new LineString([ + new Point(0, 180), + new Point(1, 179), + new Point(2, 178), + new Point(3, 177), + new Point(0, 180), + ]), + ]), + new Point(0, 180), + ], 4326); + + expect(function () use ($geometryCollection): void { + TestExtendedPlace::factory()->create(['geometry_collection' => $geometryCollection]); + })->toThrow(InvalidArgumentException::class); +}); diff --git a/tests/Objects/GeometryTest.php b/tests/Objects/GeometryTest.php index 06fc3b6..e1046d1 100644 --- a/tests/Objects/GeometryTest.php +++ b/tests/Objects/GeometryTest.php @@ -1,12 +1,15 @@ toWkb(); diff --git a/tests/Objects/LineStringTest.php b/tests/Objects/LineStringTest.php index 4cc89b8..0e4c333 100644 --- a/tests/Objects/LineStringTest.php +++ b/tests/Objects/LineStringTest.php @@ -4,7 +4,10 @@ use MatanYadaev\EloquentSpatial\Objects\LineString; use MatanYadaev\EloquentSpatial\Objects\Point; use MatanYadaev\EloquentSpatial\Objects\Polygon; +use MatanYadaev\EloquentSpatial\Tests\LaravelEloquentSpatial; +use MatanYadaev\EloquentSpatial\Tests\TestModels\TestExtendedPlace; use MatanYadaev\EloquentSpatial\Tests\TestModels\TestPlace; +use MatanYadaev\EloquentSpatial\Tests\TestObjects\ExtendedLineString; uses(DatabaseMigrations::class); @@ -160,3 +163,31 @@ expect($lineString->__toString())->toEqual('LINESTRING(180 0, 179 1)'); }); + +it('uses an extended LineString class', function (): void { + LaravelEloquentSpatial::$lineStringClass = ExtendedLineString::class; + + $lineString = new ExtendedLineString([ + new Point(0, 180), + new Point(1, 179), + ], 4326); + + /** @var TestExtendedPlace $testPlace */ + $testPlace = TestExtendedPlace::factory()->create(['line_string' => $lineString])->fresh(); + + expect($testPlace->line_string)->toBeInstanceOf(ExtendedLineString::class); + expect($testPlace->line_string)->toEqual($lineString); +}); + +it('throws exception when storing a record with regular LineString instead of the extended one', function (): void { + LaravelEloquentSpatial::$lineStringClass = ExtendedLineString::class; + + $lineString = new LineString([ + new Point(0, 180), + new Point(1, 179), + ], 4326); + + expect(function () use ($lineString): void { + TestExtendedPlace::factory()->create(['line_string' => $lineString]); + })->toThrow(InvalidArgumentException::class); +}); diff --git a/tests/Objects/MultiLineStringTest.php b/tests/Objects/MultiLineStringTest.php index 91bde31..b441e15 100644 --- a/tests/Objects/MultiLineStringTest.php +++ b/tests/Objects/MultiLineStringTest.php @@ -4,7 +4,10 @@ use MatanYadaev\EloquentSpatial\Objects\LineString; use MatanYadaev\EloquentSpatial\Objects\MultiLineString; use MatanYadaev\EloquentSpatial\Objects\Point; +use MatanYadaev\EloquentSpatial\Tests\LaravelEloquentSpatial; +use MatanYadaev\EloquentSpatial\Tests\TestModels\TestExtendedPlace; use MatanYadaev\EloquentSpatial\Tests\TestModels\TestPlace; +use MatanYadaev\EloquentSpatial\Tests\TestObjects\ExtendedMultiLineString; uses(DatabaseMigrations::class); @@ -182,3 +185,35 @@ expect($multiLineString->__toString())->toEqual('MULTILINESTRING((180 0, 179 1))'); }); + +it('uses an extended MultiLineString class', function (): void { + LaravelEloquentSpatial::$multiLineStringClass = ExtendedMultiLineString::class; + + $multiLineString = new ExtendedMultiLineString([ + new LineString([ + new Point(0, 180), + new Point(1, 179), + ]), + ], 4326); + + /** @var TestExtendedPlace $testPlace */ + $testPlace = TestExtendedPlace::factory()->create(['multi_line_string' => $multiLineString])->fresh(); + + expect($testPlace->multi_line_string)->toBeInstanceOf(ExtendedMultiLineString::class); + expect($testPlace->multi_line_string)->toEqual($multiLineString); +}); + +it('throws exception when storing a record with regular MultiLineString instead of the extended one', function (): void { + LaravelEloquentSpatial::$multiLineStringClass = ExtendedMultiLineString::class; + + $multiLineString = new MultiLineString([ + new LineString([ + new Point(0, 180), + new Point(1, 179), + ]), + ], 4326); + + expect(function () use ($multiLineString): void { + TestExtendedPlace::factory()->create(['multi_line_string' => $multiLineString]); + })->toThrow(InvalidArgumentException::class); +}); diff --git a/tests/Objects/MultiPointTest.php b/tests/Objects/MultiPointTest.php index 0420768..5b6c89b 100644 --- a/tests/Objects/MultiPointTest.php +++ b/tests/Objects/MultiPointTest.php @@ -4,7 +4,10 @@ use MatanYadaev\EloquentSpatial\Objects\MultiPoint; use MatanYadaev\EloquentSpatial\Objects\Point; use MatanYadaev\EloquentSpatial\Objects\Polygon; +use MatanYadaev\EloquentSpatial\Tests\LaravelEloquentSpatial; +use MatanYadaev\EloquentSpatial\Tests\TestModels\TestExtendedPlace; use MatanYadaev\EloquentSpatial\Tests\TestModels\TestPlace; +use MatanYadaev\EloquentSpatial\Tests\TestObjects\ExtendedMultiPoint; uses(DatabaseMigrations::class); @@ -146,3 +149,29 @@ expect($multiPoint->__toString())->toEqual('MULTIPOINT(180 0)'); }); + +it('uses an extended MultiPoint class', function (): void { + LaravelEloquentSpatial::$multiPointClass = ExtendedMultiPoint::class; + + $multiPoint = new ExtendedMultiPoint([ + new Point(0, 180), + ], 4326); + + /** @var TestExtendedPlace $testPlace */ + $testPlace = TestExtendedPlace::factory()->create(['multi_point' => $multiPoint])->fresh(); + + expect($testPlace->multi_point)->toBeInstanceOf(ExtendedMultiPoint::class); + expect($testPlace->multi_point)->toEqual($multiPoint); +}); + +it('throws exception when storing a record with regular MultiPoint instead of the extended one', function (): void { + LaravelEloquentSpatial::$multiPointClass = ExtendedMultiPoint::class; + + $multiPoint = new MultiPoint([ + new Point(0, 180), + ], 4326); + + expect(function () use ($multiPoint): void { + TestExtendedPlace::factory()->create(['multi_point' => $multiPoint]); + })->toThrow(InvalidArgumentException::class); +}); diff --git a/tests/Objects/MultiPolygonTest.php b/tests/Objects/MultiPolygonTest.php index 9d51453..052709e 100644 --- a/tests/Objects/MultiPolygonTest.php +++ b/tests/Objects/MultiPolygonTest.php @@ -5,7 +5,10 @@ use MatanYadaev\EloquentSpatial\Objects\MultiPolygon; use MatanYadaev\EloquentSpatial\Objects\Point; use MatanYadaev\EloquentSpatial\Objects\Polygon; +use MatanYadaev\EloquentSpatial\Tests\LaravelEloquentSpatial; +use MatanYadaev\EloquentSpatial\Tests\TestModels\TestExtendedPlace; use MatanYadaev\EloquentSpatial\Tests\TestModels\TestPlace; +use MatanYadaev\EloquentSpatial\Tests\TestObjects\ExtendedMultiPolygon; uses(DatabaseMigrations::class); @@ -243,3 +246,45 @@ expect($multiPolygon->__toString())->toEqual('MULTIPOLYGON(((180 0, 179 1, 178 2, 177 3, 180 0)))'); }); + +it('uses an extended MultiPolygon class', function (): void { + LaravelEloquentSpatial::$multiPolygonClass = ExtendedMultiPolygon::class; + + $multiPolygon = new ExtendedMultiPolygon([ + new Polygon([ + new LineString([ + new Point(0, 180), + new Point(1, 179), + new Point(2, 178), + new Point(3, 177), + new Point(0, 180), + ]), + ]), + ], 4326); + + /** @var TestExtendedPlace $testPlace */ + $testPlace = TestExtendedPlace::factory()->create(['multi_polygon' => $multiPolygon])->fresh(); + + expect($testPlace->multi_polygon)->toBeInstanceOf(ExtendedMultiPolygon::class); + expect($testPlace->multi_polygon)->toEqual($multiPolygon); +}); + +it('throws exception when storing a record with regular MultiPolygon instead of the extended one', function (): void { + LaravelEloquentSpatial::$multiPolygonClass = ExtendedMultiPolygon::class; + + $multiPolygon = new MultiPolygon([ + new Polygon([ + new LineString([ + new Point(0, 180), + new Point(1, 179), + new Point(2, 178), + new Point(3, 177), + new Point(0, 180), + ]), + ]), + ], 4326); + + expect(function () use ($multiPolygon): void { + TestExtendedPlace::factory()->create(['multi_polygon' => $multiPolygon]); + })->toThrow(InvalidArgumentException::class); +}); diff --git a/tests/Objects/PointTest.php b/tests/Objects/PointTest.php index ab25897..5e1fca1 100644 --- a/tests/Objects/PointTest.php +++ b/tests/Objects/PointTest.php @@ -2,7 +2,10 @@ use Illuminate\Foundation\Testing\DatabaseMigrations; use MatanYadaev\EloquentSpatial\Objects\Point; +use MatanYadaev\EloquentSpatial\Tests\LaravelEloquentSpatial; +use MatanYadaev\EloquentSpatial\Tests\TestModels\TestExtendedPlace; use MatanYadaev\EloquentSpatial\Tests\TestModels\TestPlace; +use MatanYadaev\EloquentSpatial\Tests\TestObjects\ExtendedPoint; uses(DatabaseMigrations::class); @@ -102,3 +105,25 @@ expect($point->__toString())->toEqual('POINT(180 0)'); }); + +it('uses an extended Point class', function (): void { + LaravelEloquentSpatial::$pointClass = ExtendedPoint::class; + + $point = new ExtendedPoint(0, 180, 4326); + + /** @var TestExtendedPlace $testPlace */ + $testPlace = TestExtendedPlace::factory()->create(['point' => $point])->fresh(); + + expect($testPlace->point)->toBeInstanceOf(ExtendedPoint::class); + expect($testPlace->point)->toEqual($point); +}); + +it('throws exception when storing a record with regular Point instead of the extended one', function (): void { + LaravelEloquentSpatial::$pointClass = ExtendedPoint::class; + + $point = new Point(0, 180, 4326); + + expect(function () use ($point): void { + TestExtendedPlace::factory()->create(['point' => $point]); + })->toThrow(InvalidArgumentException::class); +}); diff --git a/tests/Objects/PolygonTest.php b/tests/Objects/PolygonTest.php index d78fdc1..4914ce6 100644 --- a/tests/Objects/PolygonTest.php +++ b/tests/Objects/PolygonTest.php @@ -4,7 +4,10 @@ use MatanYadaev\EloquentSpatial\Objects\LineString; use MatanYadaev\EloquentSpatial\Objects\Point; use MatanYadaev\EloquentSpatial\Objects\Polygon; +use MatanYadaev\EloquentSpatial\Tests\LaravelEloquentSpatial; +use MatanYadaev\EloquentSpatial\Tests\TestModels\TestExtendedPlace; use MatanYadaev\EloquentSpatial\Tests\TestModels\TestPlace; +use MatanYadaev\EloquentSpatial\Tests\TestObjects\ExtendedPolygon; uses(DatabaseMigrations::class); @@ -206,7 +209,7 @@ }); it('casts a Polygon to a string', function (): void { - $polygon = $polygon = new Polygon([ + $polygon = new Polygon([ new LineString([ new Point(0, 180), new Point(1, 179), @@ -218,3 +221,41 @@ expect($polygon->__toString())->toEqual('POLYGON((180 0, 179 1, 178 2, 177 3, 180 0))'); }); + +it('uses an extended Polygon class', function (): void { + LaravelEloquentSpatial::$polygonClass = ExtendedPolygon::class; + + $polygon = new ExtendedPolygon([ + new LineString([ + new Point(0, 180), + new Point(1, 179), + new Point(2, 178), + new Point(3, 177), + new Point(0, 180), + ]), + ], 4326); + + /** @var TestExtendedPlace $testPlace */ + $testPlace = TestExtendedPlace::factory()->create(['polygon' => $polygon])->fresh(); + + expect($testPlace->polygon)->toBeInstanceOf(ExtendedPolygon::class); + expect($testPlace->polygon)->toEqual($polygon); +}); + +it('throws exception when storing a record with regular Polygon instead of the extended one', function (): void { + LaravelEloquentSpatial::$polygonClass = ExtendedPolygon::class; + + $polygon = new Polygon([ + new LineString([ + new Point(0, 180), + new Point(1, 179), + new Point(2, 178), + new Point(3, 177), + new Point(0, 180), + ]), + ], 4326); + + expect(function () use ($polygon): void { + TestExtendedPlace::factory()->create(['polygon' => $polygon]); + })->toThrow(InvalidArgumentException::class); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 9814e92..3773358 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,6 +3,13 @@ namespace MatanYadaev\EloquentSpatial\Tests; use Illuminate\Support\Facades\Config; +use MatanYadaev\EloquentSpatial\Objects\GeometryCollection; +use MatanYadaev\EloquentSpatial\Objects\LineString; +use MatanYadaev\EloquentSpatial\Objects\MultiLineString; +use MatanYadaev\EloquentSpatial\Objects\MultiPoint; +use MatanYadaev\EloquentSpatial\Objects\MultiPolygon; +use MatanYadaev\EloquentSpatial\Objects\Point; +use MatanYadaev\EloquentSpatial\Objects\Polygon; use Orchestra\Testbench\TestCase as Orchestra; class TestCase extends Orchestra @@ -11,6 +18,8 @@ protected function setUp(): void { parent::setUp(); + $this->resetGeometryClasses(); + $this->loadMigrationsFrom(__DIR__.'/database/migrations'); } @@ -25,4 +34,15 @@ public function getEnvironmentSetUp($app): void 'username' => 'root', ]); } + + protected function resetGeometryClasses(): void + { + LaravelEloquentSpatial::$pointClass = Point::class; + LaravelEloquentSpatial::$lineStringClass = LineString::class; + LaravelEloquentSpatial::$multiPointClass = MultiPoint::class; + LaravelEloquentSpatial::$polygonClass = Polygon::class; + LaravelEloquentSpatial::$multiLineStringClass = MultiLineString::class; + LaravelEloquentSpatial::$multiPolygonClass = MultiPolygon::class; + LaravelEloquentSpatial::$geometryCollectionClass = GeometryCollection::class; + } } diff --git a/tests/TestFactories/TestExtendedPlaceFactory.php b/tests/TestFactories/TestExtendedPlaceFactory.php new file mode 100644 index 0000000..6d79f14 --- /dev/null +++ b/tests/TestFactories/TestExtendedPlaceFactory.php @@ -0,0 +1,25 @@ + + */ +class TestExtendedPlaceFactory extends Factory +{ + protected $model = TestExtendedPlace::class; + + /** + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->streetName, + 'address' => $this->faker->address, + ]; + } +} diff --git a/tests/TestModels/TestExtendedPlace.php b/tests/TestModels/TestExtendedPlace.php new file mode 100644 index 0000000..5410e20 --- /dev/null +++ b/tests/TestModels/TestExtendedPlace.php @@ -0,0 +1,59 @@ + query() + */ +class TestExtendedPlace extends Model +{ + use HasFactory; + + protected $casts = [ + 'point' => ExtendedPoint::class, + 'multi_point' => ExtendedMultiPoint::class, + 'line_string' => ExtendedLineString::class, + 'multi_line_string' => ExtendedMultiLineString::class, + 'polygon' => ExtendedPolygon::class, + 'multi_polygon' => ExtendedMultiPolygon::class, + 'geometry_collection' => ExtendedGeometryCollection::class, + ]; + + /** + * @param $query + * @return SpatialBuilder + */ + public function newEloquentBuilder($query): SpatialBuilder + { + // @phpstan-ignore-next-line + return new SpatialBuilder($query); + } + + protected static function newFactory(): TestExtendedPlaceFactory + { + return TestExtendedPlaceFactory::new(); + } +} diff --git a/tests/TestObjects/ExtendedGeometryCollection.php b/tests/TestObjects/ExtendedGeometryCollection.php new file mode 100644 index 0000000..ddc5412 --- /dev/null +++ b/tests/TestObjects/ExtendedGeometryCollection.php @@ -0,0 +1,7 @@ +id(); + $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(); + }); + } + + public function down(): void + { + Schema::dropIfExists('test_places'); + } +}