Skip to content

Commit b7ab1c7

Browse files
[12.x] Add json:unicode cast to support JSON_UNESCAPED_UNICODE encoding (#54992)
* feat: 🎸 add json:unicode cast type * test: 💍 add tests * feat: 🎸 add getJsonCastFlags method * Update HasAttributes.php --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 3ba180a commit b7ab1c7

File tree

3 files changed

+85
-7
lines changed

3 files changed

+85
-7
lines changed

src/Illuminate/Database/Eloquent/Casts/Json.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ class Json
2121
/**
2222
* Encode the given value.
2323
*/
24-
public static function encode(mixed $value): mixed
24+
public static function encode(mixed $value, int $flags = 0): mixed
2525
{
26-
return isset(static::$encoder) ? (static::$encoder)($value) : json_encode($value);
26+
return isset(static::$encoder) ? (static::$encoder)($value) : json_encode($value, $flags);
2727
}
2828

2929
/**

src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ trait HasAttributes
117117
'int',
118118
'integer',
119119
'json',
120+
'json:unicode',
120121
'object',
121122
'real',
122123
'string',
@@ -837,6 +838,7 @@ protected function castAttribute($key, $value)
837838
return $this->fromJson($value, true);
838839
case 'array':
839840
case 'json':
841+
case 'json:unicode':
840842
return $this->fromJson($value);
841843
case 'collection':
842844
return new BaseCollection($this->fromJson($value));
@@ -1178,7 +1180,7 @@ public function fillJsonAttribute($key, $value)
11781180

11791181
$value = $this->asJson($this->getArrayAttributeWithValue(
11801182
$path, $key, $value
1181-
));
1183+
), $this->hasCast($key, ['json:unicode']));
11821184

11831185
$this->attributes[$key] = $this->isEncryptedCastable($key)
11841186
? $this->castAttributeAsEncryptedString($key, $value)
@@ -1313,7 +1315,7 @@ protected function getArrayAttributeByKey($key)
13131315
*/
13141316
protected function castAttributeAsJson($key, $value)
13151317
{
1316-
$value = $this->asJson($value);
1318+
$value = $this->asJson($value, $this->getJsonCastFlags($key));
13171319

13181320
if ($value === false) {
13191321
throw JsonEncodingException::forAttribute(
@@ -1324,15 +1326,33 @@ protected function castAttributeAsJson($key, $value)
13241326
return $value;
13251327
}
13261328

1329+
/**
1330+
* Get the JSON casting flags for the given attribute.
1331+
*
1332+
* @param string $key
1333+
* @return int
1334+
*/
1335+
protected function getJsonCastFlags($key)
1336+
{
1337+
$flags = 0;
1338+
1339+
if ($this->hasCast($key, ['json:unicode'])) {
1340+
$flags |= JSON_UNESCAPED_UNICODE;
1341+
}
1342+
1343+
return $flags;
1344+
}
1345+
13271346
/**
13281347
* Encode the given value as JSON.
13291348
*
13301349
* @param mixed $value
1350+
* @param int $flags
13311351
* @return string
13321352
*/
1333-
protected function asJson($value)
1353+
protected function asJson($value, $flags = 0)
13341354
{
1335-
return Json::encode($value);
1355+
return Json::encode($value, $flags);
13361356
}
13371357

13381358
/**
@@ -1669,7 +1689,7 @@ protected function isDateCastableWithCustomFormat($key)
16691689
*/
16701690
protected function isJsonCastable($key)
16711691
{
1672-
return $this->hasCast($key, ['array', 'json', 'object', 'collection', 'encrypted:array', 'encrypted:collection', 'encrypted:json', 'encrypted:object']);
1692+
return $this->hasCast($key, ['array', 'json', 'json:unicode', 'object', 'collection', 'encrypted:array', 'encrypted:collection', 'encrypted:json', 'encrypted:object']);
16731693
}
16741694

16751695
/**

tests/Database/DatabaseEloquentModelTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2472,6 +2472,7 @@ public function testModelAttributesAreCastedWhenPresentInCastsPropertyOrCastsMet
24722472
$obj->foo = 'bar';
24732473
$model->arrayAttribute = $obj;
24742474
$model->jsonAttribute = ['foo' => 'bar'];
2475+
$model->jsonAttributeWithUnicode = ['こんにちは' => '世界'];
24752476
$model->dateAttribute = '1969-07-20';
24762477
$model->datetimeAttribute = '1969-07-20 22:56:00';
24772478
$model->timestampAttribute = '1969-07-20 22:56:00';
@@ -2486,12 +2487,15 @@ public function testModelAttributesAreCastedWhenPresentInCastsPropertyOrCastsMet
24862487
$this->assertIsObject($model->objectAttribute);
24872488
$this->assertIsArray($model->arrayAttribute);
24882489
$this->assertIsArray($model->jsonAttribute);
2490+
$this->assertIsArray($model->jsonAttributeWithUnicode);
24892491
$this->assertTrue($model->boolAttribute);
24902492
$this->assertFalse($model->booleanAttribute);
24912493
$this->assertEquals($obj, $model->objectAttribute);
24922494
$this->assertEquals(['foo' => 'bar'], $model->arrayAttribute);
24932495
$this->assertEquals(['foo' => 'bar'], $model->jsonAttribute);
24942496
$this->assertSame('{"foo":"bar"}', $model->jsonAttributeValue());
2497+
$this->assertEquals(['こんにちは' => '世界'], $model->jsonAttributeWithUnicode);
2498+
$this->assertSame('{"こんにちは":"世界"}', $model->jsonAttributeWithUnicodeValue());
24952499
$this->assertInstanceOf(Carbon::class, $model->dateAttribute);
24962500
$this->assertInstanceOf(Carbon::class, $model->datetimeAttribute);
24972501
$this->assertInstanceOf(BaseCollection::class, $model->collectionAttribute);
@@ -2510,12 +2514,14 @@ public function testModelAttributesAreCastedWhenPresentInCastsPropertyOrCastsMet
25102514
$this->assertIsObject($arr['objectAttribute']);
25112515
$this->assertIsArray($arr['arrayAttribute']);
25122516
$this->assertIsArray($arr['jsonAttribute']);
2517+
$this->assertIsArray($arr['jsonAttributeWithUnicode']);
25132518
$this->assertIsArray($arr['collectionAttribute']);
25142519
$this->assertTrue($arr['boolAttribute']);
25152520
$this->assertFalse($arr['booleanAttribute']);
25162521
$this->assertEquals($obj, $arr['objectAttribute']);
25172522
$this->assertEquals(['foo' => 'bar'], $arr['arrayAttribute']);
25182523
$this->assertEquals(['foo' => 'bar'], $arr['jsonAttribute']);
2524+
$this->assertEquals(['こんにちは' => '世界'], $arr['jsonAttributeWithUnicode']);
25192525
$this->assertSame('1969-07-20 00:00:00', $arr['dateAttribute']);
25202526
$this->assertSame('1969-07-20 22:56:00', $arr['datetimeAttribute']);
25212527
$this->assertEquals(-14173440, $arr['timestampAttribute']);
@@ -2544,6 +2550,7 @@ public function testModelAttributeCastingPreservesNull()
25442550
$model->objectAttribute = null;
25452551
$model->arrayAttribute = null;
25462552
$model->jsonAttribute = null;
2553+
$model->jsonAttributeWithUnicode = null;
25472554
$model->dateAttribute = null;
25482555
$model->datetimeAttribute = null;
25492556
$model->timestampAttribute = null;
@@ -2559,6 +2566,7 @@ public function testModelAttributeCastingPreservesNull()
25592566
$this->assertNull($attributes['objectAttribute']);
25602567
$this->assertNull($attributes['arrayAttribute']);
25612568
$this->assertNull($attributes['jsonAttribute']);
2569+
$this->assertNull($attributes['jsonAttributeWithUnicode']);
25622570
$this->assertNull($attributes['dateAttribute']);
25632571
$this->assertNull($attributes['datetimeAttribute']);
25642572
$this->assertNull($attributes['timestampAttribute']);
@@ -2572,6 +2580,7 @@ public function testModelAttributeCastingPreservesNull()
25722580
$this->assertNull($model->objectAttribute);
25732581
$this->assertNull($model->arrayAttribute);
25742582
$this->assertNull($model->jsonAttribute);
2583+
$this->assertNull($model->jsonAttributeWithUnicode);
25752584
$this->assertNull($model->dateAttribute);
25762585
$this->assertNull($model->datetimeAttribute);
25772586
$this->assertNull($model->timestampAttribute);
@@ -2587,6 +2596,7 @@ public function testModelAttributeCastingPreservesNull()
25872596
$this->assertNull($array['objectAttribute']);
25882597
$this->assertNull($array['arrayAttribute']);
25892598
$this->assertNull($array['jsonAttribute']);
2599+
$this->assertNull($array['jsonAttributeWithUnicode']);
25902600
$this->assertNull($array['dateAttribute']);
25912601
$this->assertNull($array['datetimeAttribute']);
25922602
$this->assertNull($array['timestampAttribute']);
@@ -2603,11 +2613,45 @@ public function testModelAttributeCastingFailsOnUnencodableData()
26032613
$obj = new stdClass;
26042614
$obj->foo = "b\xF8r";
26052615
$model->arrayAttribute = $obj;
2616+
2617+
$model->getAttributes();
2618+
}
2619+
2620+
public function testModelJsonCastingFailsOnUnencodableData()
2621+
{
2622+
$this->expectException(JsonEncodingException::class);
2623+
$this->expectExceptionMessage('Unable to encode attribute [jsonAttribute] for model [Illuminate\Tests\Database\EloquentModelCastingStub] to JSON: Malformed UTF-8 characters, possibly incorrectly encoded.');
2624+
2625+
$model = new EloquentModelCastingStub;
26062626
$model->jsonAttribute = ['foo' => "b\xF8r"];
26072627

26082628
$model->getAttributes();
26092629
}
26102630

2631+
public function testModelAttributeCastingFailsOnUnencodableDataWithUnicode()
2632+
{
2633+
$this->expectException(JsonEncodingException::class);
2634+
$this->expectExceptionMessage('Unable to encode attribute [jsonAttributeWithUnicode] for model [Illuminate\Tests\Database\EloquentModelCastingStub] to JSON: Malformed UTF-8 characters, possibly incorrectly encoded.');
2635+
2636+
$model = new EloquentModelCastingStub;
2637+
$model->jsonAttributeWithUnicode = ['foo' => "b\xF8r"];
2638+
2639+
$model->getAttributes();
2640+
}
2641+
2642+
public function testJsonCastingRespectsUnicodeOption()
2643+
{
2644+
$data = ['こんにちは' => '世界'];
2645+
$model = new EloquentModelCastingStub;
2646+
$model->jsonAttribute = $data;
2647+
$model->jsonAttributeWithUnicode = $data;
2648+
2649+
$this->assertSame('{"\u3053\u3093\u306b\u3061\u306f":"\u4e16\u754c"}', $model->jsonAttributeValue());
2650+
$this->assertSame('{"こんにちは":"世界"}', $model->jsonAttributeWithUnicodeValue());
2651+
$this->assertSame(['こんにちは' => '世界'], $model->jsonAttribute);
2652+
$this->assertSame(['こんにちは' => '世界'], $model->jsonAttributeWithUnicode);
2653+
}
2654+
26112655
public function testModelAttributeCastingWithFloats()
26122656
{
26132657
$model = new EloquentModelCastingStub;
@@ -3028,6 +3072,7 @@ public function testGetOriginalCastsAttributes()
30283072
$collection = collect($array);
30293073
$model->arrayAttribute = $array;
30303074
$model->jsonAttribute = $array;
3075+
$model->jsonAttributeWithUnicode = $array;
30313076
$model->collectionAttribute = $collection;
30323077

30333078
$model->syncOriginal();
@@ -3044,6 +3089,9 @@ public function testGetOriginalCastsAttributes()
30443089
$model->jsonAttribute = [
30453090
'foo' => 'bar2',
30463091
];
3092+
$model->jsonAttributeWithUnicode = [
3093+
'foo' => 'bar2',
3094+
];
30473095
$model->collectionAttribute = collect([
30483096
'foo' => 'bar2',
30493097
]);
@@ -3080,6 +3128,10 @@ public function testGetOriginalCastsAttributes()
30803128
$this->assertEquals(['foo' => 'bar'], $model->getOriginal('jsonAttribute'));
30813129
$this->assertEquals(['foo' => 'bar2'], $model->getAttribute('jsonAttribute'));
30823130

3131+
$this->assertEquals($array, $model->getOriginal('jsonAttributeWithUnicode'));
3132+
$this->assertEquals(['foo' => 'bar'], $model->getOriginal('jsonAttributeWithUnicode'));
3133+
$this->assertEquals(['foo' => 'bar2'], $model->getAttribute('jsonAttributeWithUnicode'));
3134+
30833135
$this->assertEquals(['foo' => 'bar'], $model->getOriginal('collectionAttribute')->toArray());
30843136
$this->assertEquals(['foo' => 'bar2'], $model->getAttribute('collectionAttribute')->toArray());
30853137
}
@@ -3596,6 +3648,7 @@ class EloquentModelCastingStub extends Model
35963648
'boolAttribute' => 'bool',
35973649
'objectAttribute' => 'object',
35983650
'jsonAttribute' => 'json',
3651+
'jsonAttributeWithUnicode' => 'json:unicode',
35993652
'dateAttribute' => 'date',
36003653
'timestampAttribute' => 'timestamp',
36013654
'ascollectionAttribute' => AsCollection::class,
@@ -3633,6 +3686,11 @@ public function jsonAttributeValue()
36333686
return $this->attributes['jsonAttribute'];
36343687
}
36353688

3689+
public function jsonAttributeWithUnicodeValue()
3690+
{
3691+
return $this->attributes['jsonAttributeWithUnicode'];
3692+
}
3693+
36363694
protected function serializeDate(DateTimeInterface $date)
36373695
{
36383696
return $date->format('Y-m-d H:i:s');

0 commit comments

Comments
 (0)