From e82a9944a8314057a8b550b0502e23e4cb2d5af1 Mon Sep 17 00:00:00 2001 From: Haitham Date: Sun, 20 Sep 2026 01:28:06 -0400 Subject: [PATCH] Wrap typed polygons in SpatialExpression like the other spatial casts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Polygon::set() returned the bare Polygon object for typed geometries while Point and MultiPolygon wrap them in SpatialExpression. Laravel re-runs class casts during getAttributes(), which overwrote the SpatialExpression prepared by SpatialTrait::performInsert() — so the raw geometry was bound to the query and MySQL rejected the write with error 1416 (Cannot get geometry object from data you send to the GEOMETRY field). This broke zone creation, e.g. in the FleetOps testing seeders. --- server/src/Casts/Polygon.php | 10 ++++++---- server/tests/Unit/Casts/SpatialCastBranchesTest.php | 7 +++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/server/src/Casts/Polygon.php b/server/src/Casts/Polygon.php index 181fd6107..7c1cb482f 100644 --- a/server/src/Casts/Polygon.php +++ b/server/src/Casts/Polygon.php @@ -32,18 +32,20 @@ public function get($model, $key, $value, $attributes) public function set($model, $key, $value, $attributes) { // Checked before the broader GeometryInterface guard below, which a - // SpatialPolygon also satisfies — otherwise this arm never fires. Both - // arms behave identically, so ordering does not change what is stored. + // SpatialPolygon also satisfies — otherwise this arm never fires. + // It must wrap in a SpatialExpression exactly as that guard does: + // returning the bare geometry here would change what is bound on writes + // that skip SpatialTrait::performInsert(). if ($value instanceof SpatialPolygon) { $model->geometries[$key] = $value; - return $value; + return new SpatialExpression($value); } if ($value instanceof GeometryInterface) { $model->geometries[$key] = $value; - return $value; + return new SpatialExpression($value); } if (Utils::isGeoJson($value)) { diff --git a/server/tests/Unit/Casts/SpatialCastBranchesTest.php b/server/tests/Unit/Casts/SpatialCastBranchesTest.php index 4fb21d19e..7671a8f2e 100644 --- a/server/tests/Unit/Casts/SpatialCastBranchesTest.php +++ b/server/tests/Unit/Casts/SpatialCastBranchesTest.php @@ -38,12 +38,15 @@ function fleetopsSpatialCastSquare(): SpatialPolygon expect($returned)->toBeInstanceOf(SpatialExpression::class) ->and($model->geometries['border'])->toBe($multi); - // The polygon cast stashes the geometry but hands the value back directly + // The polygon cast wraps the geometry like every other spatial cast — + // returning the bare object lets class-cast re-serialisation overwrite the + // SpatialExpression that SpatialTrait::performInsert() prepared, so the + // raw geometry gets bound and MySQL rejects it with error 1416 $polygonModel = new FleetOpsSpatialCastModel(); $polygon = fleetopsSpatialCastSquare(); $polygonCast = (new Fleetbase\FleetOps\Casts\Polygon())->set($polygonModel, 'border', $polygon, []); - expect($polygonCast)->toBe($polygon) + expect($polygonCast)->toBeInstanceOf(SpatialExpression::class) ->and($polygonModel->geometries['border'])->toBe($polygon); // Expressions are already bind-ready, so the point cast returns them