diff --git a/composer.json b/composer.json index 2420fa26..f9ac9e4f 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=5.5", + "php": ">=5.5.9", "ext-pdo": "*", "ext-json": "*", "illuminate/database": "^5.2|^6.0|^7.0", @@ -23,7 +23,7 @@ "jmikola/geojson": "^1.0" }, "require-dev": { - "phpunit/phpunit": "~4.8||~5.7", + "phpunit/phpunit": "~4.8|~5.7", "mockery/mockery": "^0.9.9", "laravel/laravel": "^5.2|^6.0|^7.0", "doctrine/dbal": "^2.5", diff --git a/src/Types/GeometryCollection.php b/src/Types/GeometryCollection.php index 2e1f0321..4311685a 100755 --- a/src/Types/GeometryCollection.php +++ b/src/Types/GeometryCollection.php @@ -14,6 +14,20 @@ class GeometryCollection extends Geometry implements IteratorAggregate, ArrayAccess, Arrayable, Countable { + /** + * The minimum number of items required to create this collection. + * + * @var int + */ + protected $minimumCollectionItems = 0; + + /** + * The class of the items in the collection. + * + * @var string + */ + protected $collectionItemType = GeometryInterface::class; + /** * The items contained in the spatial collection. * @@ -28,13 +42,7 @@ class GeometryCollection extends Geometry implements IteratorAggregate, ArrayAcc */ public function __construct(array $geometries) { - $validated = array_filter($geometries, function ($value) { - return $value instanceof GeometryInterface; - }); - - if (count($geometries) !== count($validated)) { - throw new InvalidArgumentException('$geometries must be an array of Geometry objects'); - } + $this->validateItems($geometries); $this->items = $geometries; } @@ -58,6 +66,10 @@ public function __toString() public static function fromString($wktArgument) { + if (empty($wktArgument)) { + return new static([]); + } + $geometry_strings = preg_split('/,\s*(?=[A-Za-z])/', $wktArgument); return new static(array_map(function ($geometry_string) { @@ -89,9 +101,7 @@ public function offsetGet($offset) public function offsetSet($offset, $value) { - if (!($value instanceof GeometryInterface)) { - throw new InvalidArgumentException('$value must be an instance of GeometryInterface'); - } + $this->validateItemType($value); if (is_null($offset)) { $this->items[] = $value; @@ -142,4 +152,57 @@ public function jsonSerialize() return new \GeoJson\Geometry\GeometryCollection($geometries); } + + /** + * Checks whether the items are valid to create this collection. + * + * @param array $items + */ + protected function validateItems(array $items) + { + $this->validateItemCount($items); + + foreach ($items as $item) { + $this->validateItemType($item); + } + } + + /** + * Checks whether the array has enough items to generate a valid WKT. + * + * @param array $items + * + * @see $minimumCollectionItems + */ + protected function validateItemCount(array $items) + { + if (count($items) < $this->minimumCollectionItems) { + $entries = $this->minimumCollectionItems === 1 ? 'entry' : 'entries'; + + throw new InvalidArgumentException(sprintf( + '%s must contain at least %d %s', + get_class($this), + $this->minimumCollectionItems, + $entries + )); + } + } + + /** + * Checks the type of the items in the array. + * + * @param $item + * + * @see $collectionItemType + */ + protected function validateItemType($item) + { + if (!$item instanceof $this->collectionItemType) { + throw new InvalidArgumentException(sprintf( + '%s must be a collection of %s', + get_class($this), + $this->collectionItemType + )); + } + } } diff --git a/src/Types/LineString.php b/src/Types/LineString.php index 02097edd..8c226444 100644 --- a/src/Types/LineString.php +++ b/src/Types/LineString.php @@ -8,6 +8,13 @@ class LineString extends PointCollection { + /** + * The minimum number of items required to create this collection. + * + * @var int + */ + protected $minimumCollectionItems = 2; + public function toWKT() { return sprintf('LINESTRING(%s)', $this->toPairList()); diff --git a/src/Types/MultiLineString.php b/src/Types/MultiLineString.php index dd3342fd..dd815ebf 100644 --- a/src/Types/MultiLineString.php +++ b/src/Types/MultiLineString.php @@ -5,29 +5,22 @@ use GeoJson\GeoJson; use GeoJson\Geometry\MultiLineString as GeoJsonMultiLineString; use Grimzy\LaravelMysqlSpatial\Exceptions\InvalidGeoJsonException; -use InvalidArgumentException; class MultiLineString extends GeometryCollection { /** - * @param LineString[] $lineStrings + * The minimum number of items required to create this collection. + * + * @var int */ - public function __construct(array $lineStrings) - { - if (count($lineStrings) < 1) { - throw new InvalidArgumentException('$lineStrings must contain at least one entry'); - } - - $validated = array_filter($lineStrings, function ($value) { - return $value instanceof LineString; - }); - - if (count($lineStrings) !== count($validated)) { - throw new InvalidArgumentException('$lineStrings must be an array of LineString'); - } + protected $minimumCollectionItems = 1; - parent::__construct($lineStrings); - } + /** + * The class of the items in the collection. + * + * @var string + */ + protected $collectionItemType = LineString::class; public function getLineStrings() { @@ -58,9 +51,7 @@ public function __toString() public function offsetSet($offset, $value) { - if (!($value instanceof LineString)) { - throw new InvalidArgumentException('$value must be an instance of LineString'); - } + $this->validateItemType($value); parent::offsetSet($offset, $value); } diff --git a/src/Types/MultiPoint.php b/src/Types/MultiPoint.php index fb55f9e8..eafa7c0e 100644 --- a/src/Types/MultiPoint.php +++ b/src/Types/MultiPoint.php @@ -8,6 +8,13 @@ class MultiPoint extends PointCollection { + /** + * The minimum number of items required to create this collection. + * + * @var int + */ + protected $minimumCollectionItems = 1; + public function toWKT() { return sprintf('MULTIPOINT(%s)', (string) $this); diff --git a/src/Types/MultiPolygon.php b/src/Types/MultiPolygon.php index aba2b6d1..0a5b6784 100644 --- a/src/Types/MultiPolygon.php +++ b/src/Types/MultiPolygon.php @@ -5,24 +5,22 @@ use GeoJson\GeoJson; use GeoJson\Geometry\MultiPolygon as GeoJsonMultiPolygon; use Grimzy\LaravelMysqlSpatial\Exceptions\InvalidGeoJsonException; -use InvalidArgumentException; class MultiPolygon extends GeometryCollection { /** - * @param Polygon[] $polygons + * The minimum number of items required to create this collection. + * + * @var int */ - public function __construct(array $polygons) - { - $validated = array_filter($polygons, function ($value) { - return $value instanceof Polygon; - }); + protected $minimumCollectionItems = 1; - if (count($polygons) !== count($validated)) { - throw new InvalidArgumentException('$polygons must be an array of Polygon'); - } - parent::__construct($polygons); - } + /** + * The class of the items in the collection. + * + * @var string + */ + protected $collectionItemType = Polygon::class; public function toWKT() { @@ -93,9 +91,7 @@ protected static function assembleParts(array $parts) public function offsetSet($offset, $value) { - if (!($value instanceof Polygon)) { - throw new InvalidArgumentException('$value must be an instance of Polygon'); - } + $this->validateItemType($value); parent::offsetSet($offset, $value); } diff --git a/src/Types/PointCollection.php b/src/Types/PointCollection.php index af586b28..30d1b8de 100755 --- a/src/Types/PointCollection.php +++ b/src/Types/PointCollection.php @@ -8,24 +8,11 @@ abstract class PointCollection extends GeometryCollection { /** - * @param Point[] $points + * The class of the items in the collection. + * + * @var string */ - public function __construct(array $points) - { - if (count($points) < 2) { - throw new InvalidArgumentException('$points must contain at least two entries'); - } - - $validated = array_filter($points, function ($value) { - return $value instanceof Point; - }); - - if (count($points) !== count($validated)) { - throw new InvalidArgumentException('$points must be an array of Points'); - } - - parent::__construct($points); - } + protected $collectionItemType = Point::class; public function toPairList() { @@ -36,9 +23,7 @@ public function toPairList() public function offsetSet($offset, $value) { - if (!($value instanceof Point)) { - throw new InvalidArgumentException('$value must be an instance of Point'); - } + $this->validateItemType($value); parent::offsetSet($offset, $value); } diff --git a/tests/Integration/SpatialTest.php b/tests/Integration/SpatialTest.php index 70fa3771..415d8557 100644 --- a/tests/Integration/SpatialTest.php +++ b/tests/Integration/SpatialTest.php @@ -242,6 +242,17 @@ public function testInsertGeometryCollection() $this->assertDatabaseHas('geometry', ['id' => $geo->id]); } + public function testInsertEmptyGeometryCollection() + { + $geo = new GeometryModel(); + + $geo->location = new Point(1, 2); + + $geo->multi_geometries = new GeometryCollection([]); + $geo->save(); + $this->assertDatabaseHas('geometry', ['id' => $geo->id]); + } + public function testUpdate() { $geo = new GeometryModel(); diff --git a/tests/Unit/BaseTestCase.php b/tests/Unit/BaseTestCase.php index 44f323c5..0b53a414 100644 --- a/tests/Unit/BaseTestCase.php +++ b/tests/Unit/BaseTestCase.php @@ -7,12 +7,14 @@ public function tearDown() Mockery::close(); } - protected function assertException($exceptionName) + protected function assertException($exceptionName, $exceptionMessage = '', $exceptionCode = 0) { if (method_exists(parent::class, 'expectException')) { parent::expectException($exceptionName); + parent::expectExceptionMessage($exceptionMessage); + parent::expectExceptionCode($exceptionCode); } else { - $this->setExpectedException($exceptionName); + $this->setExpectedException($exceptionName, $exceptionMessage, $exceptionCode); } } } diff --git a/tests/Unit/Eloquent/SpatialTraitTest.php b/tests/Unit/Eloquent/SpatialTraitTest.php index 717e8897..f59913f3 100644 --- a/tests/Unit/Eloquent/SpatialTraitTest.php +++ b/tests/Unit/Eloquent/SpatialTraitTest.php @@ -484,7 +484,10 @@ public function testScopeDoesTouch() public function testScopeOrderBySpatialThrowsExceptionWhenFunctionNotRegistered() { $point = new Point(1, 2); - $this->assertException(\Grimzy\LaravelMysqlSpatial\Exceptions\UnknownSpatialFunctionException::class); + $this->assertException( + \Grimzy\LaravelMysqlSpatial\Exceptions\UnknownSpatialFunctionException::class, + 'does-not-exist' + ); TestModel::orderBySpatial('point', $point, 'does-not-exist'); } diff --git a/tests/Unit/Types/GeometryCollectionTest.php b/tests/Unit/Types/GeometryCollectionTest.php index 07759c5f..4576a77b 100644 --- a/tests/Unit/Types/GeometryCollectionTest.php +++ b/tests/Unit/Types/GeometryCollectionTest.php @@ -33,9 +33,34 @@ public function testJsonSerialize() $this->assertSame('{"type":"GeometryCollection","geometries":[{"type":"LineString","coordinates":[[0,0],[1,0],[1,1],[0,1],[0,0]]},{"type":"Point","coordinates":[200,100]}]}', json_encode($this->getGeometryCollection()->jsonSerialize())); } + public function testCanCreateEmptyGeometryCollection() + { + $geometryCollection = new GeometryCollection([]); + $this->assertInstanceOf(GeometryCollection::class, $geometryCollection); + } + + public function testFromWKTWithEmptyGeometryCollection() + { + /** + * @var GeometryCollection + */ + $geometryCollection = GeometryCollection::fromWKT('GEOMETRYCOLLECTION()'); + $this->assertInstanceOf(GeometryCollection::class, $geometryCollection); + + $this->assertEquals(0, $geometryCollection->count()); + } + + public function testToWKTWithEmptyGeometryCollection() + { + $this->assertEquals('GEOMETRYCOLLECTION()', (new GeometryCollection([]))->toWKT()); + } + public function testInvalidArgumentExceptionNotArrayGeometries() { - $this->assertException(InvalidArgumentException::class); + $this->assertException( + InvalidArgumentException::class, + 'Grimzy\LaravelMysqlSpatial\Types\GeometryCollection must be a collection of Grimzy\LaravelMysqlSpatial\Types\GeometryInterface' + ); $geometrycollection = new GeometryCollection([ $this->getPoint(), 1, @@ -85,7 +110,10 @@ public function testArrayAccess() $this->assertEquals($point100, $geometryCollection[100]); // assert invalid - $this->assertException(InvalidArgumentException::class); + $this->assertException( + InvalidArgumentException::class, + 'Grimzy\LaravelMysqlSpatial\Types\GeometryCollection must be a collection of Grimzy\LaravelMysqlSpatial\Types\GeometryInterface' + ); $geometryCollection[] = 1; } diff --git a/tests/Unit/Types/GeometryTest.php b/tests/Unit/Types/GeometryTest.php index 8bd765a7..e51022d5 100644 --- a/tests/Unit/Types/GeometryTest.php +++ b/tests/Unit/Types/GeometryTest.php @@ -32,7 +32,10 @@ public function testGetWKTClass() $this->assertEquals(MultiLineString::class, Geometry::getWKTClass('MULTILINESTRING((0 0,1 1,1 2),(2 3,3 2,5 4))')); $this->assertEquals(MultiPolygon::class, Geometry::getWKTClass('MULTIPOLYGON(((0 0,4 0,4 4,0 4,0 0),(1 1,2 1,2 2,1 2,1 1)), ((-1 -1,-1 -2,-2 -2,-2 -1,-1 -1)))')); $this->assertEquals(GeometryCollection::class, Geometry::getWKTClass('GEOMETRYCOLLECTION(POINT(2 3),LINESTRING(2 3,3 4))')); - $this->assertException(UnknownWKTTypeException::class); + $this->assertException( + UnknownWKTTypeException::class, + 'Type was TRIANGLE' + ); Geometry::getWKTClass('TRIANGLE((0 0, 0 9, 9 0, 0 0))'); } diff --git a/tests/Unit/Types/MultiLineStringTest.php b/tests/Unit/Types/MultiLineStringTest.php index df7b42e4..563ecbd1 100644 --- a/tests/Unit/Types/MultiLineStringTest.php +++ b/tests/Unit/Types/MultiLineStringTest.php @@ -59,13 +59,19 @@ public function testJsonSerialize() public function testInvalidArgumentExceptionAtLeastOneEntry() { - $this->assertException(InvalidArgumentException::class); + $this->assertException( + InvalidArgumentException::class, + 'Grimzy\LaravelMysqlSpatial\Types\MultiLineString must contain at least 1 entry' + ); $multilinestring = new MultiLineString([]); } public function testInvalidArgumentExceptionNotArrayOfLineString() { - $this->assertException(InvalidArgumentException::class); + $this->assertException( + InvalidArgumentException::class, + 'Grimzy\LaravelMysqlSpatial\Types\MultiLineString must be a collection of Grimzy\LaravelMysqlSpatial\Types\LineString' + ); $multilinestring = new MultiLineString([ new LineString([new Point(0, 0), new Point(1, 1)]), new Point(0, 1), @@ -98,7 +104,10 @@ public function testArrayAccess() $this->assertEquals($linestring2, $multilinestring[2]); // assert invalid - $this->assertException(InvalidArgumentException::class); + $this->assertException( + InvalidArgumentException::class, + 'Grimzy\LaravelMysqlSpatial\Types\MultiLineString must be a collection of Grimzy\LaravelMysqlSpatial\Types\LineString' + ); $multilinestring[] = 1; } } diff --git a/tests/Unit/Types/MultiPointTest.php b/tests/Unit/Types/MultiPointTest.php index 1e9bf7f4..6e94af87 100644 --- a/tests/Unit/Types/MultiPointTest.php +++ b/tests/Unit/Types/MultiPointTest.php @@ -58,13 +58,19 @@ public function testJsonSerialize() public function testInvalidArgumentExceptionAtLeastOneEntry() { - $this->assertException(InvalidArgumentException::class); + $this->assertException( + InvalidArgumentException::class, + 'Grimzy\LaravelMysqlSpatial\Types\MultiPoint must contain at least 1 entry' + ); $multipoint = new MultiPoint([]); } public function testInvalidArgumentExceptionNotArrayOfLineString() { - $this->assertException(InvalidArgumentException::class); + $this->assertException( + InvalidArgumentException::class, + 'Grimzy\LaravelMysqlSpatial\Types\MultiPoint must be a collection of Grimzy\LaravelMysqlSpatial\Types\Point' + ); $multipoint = new MultiPoint([ new Point(0, 0), 1, @@ -87,7 +93,10 @@ public function testArrayAccess() $this->assertEquals($point2, $multipoint[2]); // assert invalid - $this->assertException(InvalidArgumentException::class); + $this->assertException( + InvalidArgumentException::class, + 'Grimzy\LaravelMysqlSpatial\Types\MultiPoint must be a collection of Grimzy\LaravelMysqlSpatial\Types\Point' + ); $multipoint[] = 1; } @@ -132,7 +141,10 @@ public function testDeprecatedInsertPoint() $this->assertEquals($point2, $multipoint[1]); $this->assertEquals($point3, $multipoint[2]); - $this->assertException(InvalidArgumentException::class); + $this->assertException( + InvalidArgumentException::class, + '$index is greater than the size of the array' + ); $multipoint->insertPoint(100, new Point(100, 100)); } } diff --git a/tests/Unit/Types/MultiPolygonTest.php b/tests/Unit/Types/MultiPolygonTest.php index cf5894ff..a37df382 100644 --- a/tests/Unit/Types/MultiPolygonTest.php +++ b/tests/Unit/Types/MultiPolygonTest.php @@ -75,9 +75,21 @@ public function testJsonSerialize() $this->assertSame('{"type":"MultiPolygon","coordinates":[[[[0,0],[1,0],[1,1],[0,1],[0,0]],[[10,10],[20,10],[20,20],[10,20],[10,10]]],[[[100,100],[200,100],[200,200],[100,200],[100,100]]]]}', json_encode($this->getMultiPolygon())); } - public function testInvalidArgumentExceptionNotArrayOfLineString() + public function testInvalidArgumentExceptionAtLeastOneEntry() { - $this->assertException(InvalidArgumentException::class); + $this->assertException( + InvalidArgumentException::class, + 'Grimzy\LaravelMysqlSpatial\Types\MultiPolygon must contain at least 1 entry' + ); + $multipolygon = new MultiPolygon([]); + } + + public function testInvalidArgumentExceptionNotArrayOfPolygon() + { + $this->assertException( + InvalidArgumentException::class, + 'Grimzy\LaravelMysqlSpatial\Types\MultiPolygon must be a collection of Grimzy\LaravelMysqlSpatial\Types\Polygon' + ); $multipolygon = new MultiPolygon([ $this->getPolygon1(), $this->getLineString1(), @@ -101,7 +113,10 @@ public function testArrayAccess() $this->assertEquals($polygon2, $multipolygon[2]); // assert invalid - $this->assertException(InvalidArgumentException::class); + $this->assertException( + InvalidArgumentException::class, + 'Grimzy\LaravelMysqlSpatial\Types\MultiPolygon must be a collection of Grimzy\LaravelMysqlSpatial\Types\Polygon' + ); $multipolygon[] = 1; }