From 34686ce56b57e74e001f724492709cc0f6fd98fe Mon Sep 17 00:00:00 2001 From: Usman Khan Date: Fri, 28 Aug 2026 21:32:38 +0500 Subject: [PATCH] Fail loudly when attribute data cannot be encoded --- .../core/src/Base/Casts/AsAttributeData.php | 7 ++- .../Unit/Base/Casts/AsAttributeDataTest.php | 52 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 tests/core/Unit/Base/Casts/AsAttributeDataTest.php diff --git a/packages/core/src/Base/Casts/AsAttributeData.php b/packages/core/src/Base/Casts/AsAttributeData.php index c0eb262b0a..63e5541927 100644 --- a/packages/core/src/Base/Casts/AsAttributeData.php +++ b/packages/core/src/Base/Casts/AsAttributeData.php @@ -53,7 +53,12 @@ public function set($model, $key, $value, $attributes) ]; } - return [$key => json_encode($data)]; + // JSON_THROW_ON_ERROR, because json_encode() reports failure by + // returning false - on a value that is not valid UTF-8, or that + // exceeds max depth. Bound into the update, that false is stored + // as 0, which both destroys the attributes the row already held + // and leaves it unreadable, since get() cannot iterate an int. + return [$key => json_encode($data, JSON_THROW_ON_ERROR)]; } }; } diff --git a/tests/core/Unit/Base/Casts/AsAttributeDataTest.php b/tests/core/Unit/Base/Casts/AsAttributeDataTest.php new file mode 100644 index 0000000000..5ec18ad458 --- /dev/null +++ b/tests/core/Unit/Base/Casts/AsAttributeDataTest.php @@ -0,0 +1,52 @@ +create([ + 'attribute_data' => collect([ + 'name' => new TranslatedText(['en' => 'A name worth keeping']), + ]), + ]); + + $before = DB::table('lunar_products')->where('id', $product->id)->value('attribute_data'); + + // A single 0xE4 byte - "a" with an umlaut, as Latin-1 writes it. Any feed + // that is not UTF-8 produces these. The cast runs on assignment, so this + // never reaches the database at all. + expect(function () use ($product) { + $product->attribute_data = collect([ + 'name' => new TranslatedText(['en' => "Sh\xE4mpoo"]), + ]); + + $product->save(); + })->toThrow(JsonException::class); + + $after = DB::table('lunar_products')->where('id', $product->id)->value('attribute_data'); + + // The point is not that it failed, but that it failed without taking the + // existing attributes with it. + expect($after)->toEqual($before); + + expect(Product::find($product->id)->translateAttribute('name')) + ->toEqual('A name worth keeping'); +}); + +test('can store attribute data containing multibyte characters', function () { + $product = Product::factory()->create([ + 'attribute_data' => collect([ + 'name' => new TranslatedText(['en' => 'Crème brûlée · 日本語 · emoji 🧴']), + ]), + ]); + + expect(Product::find($product->id)->translateAttribute('name')) + ->toEqual('Crème brûlée · 日本語 · emoji 🧴'); +});