Skip to content

Commit 4b516e1

Browse files
authored
fix: Entity::normalizeValue() must handle UnitEnum before toArray() (codeigniter4#10137)
1 parent 8533ba6 commit 4b516e1

7 files changed

Lines changed: 240 additions & 5 deletions

File tree

system/Entity/Entity.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,11 @@ private function normalizeValue(mixed $data): mixed
458458
// Check for Entity instance (use raw values, recursive)
459459
if ($data instanceof self) {
460460
$objectData = $data->toRawArray(false, true);
461+
} elseif ($data instanceof UnitEnum) {
462+
return [
463+
'__class' => $data::class,
464+
'__enum' => $data instanceof BackedEnum ? $data->value : $data->name,
465+
];
461466
} elseif ($data instanceof JsonSerializable) {
462467
$objectData = $data->jsonSerialize();
463468
} elseif (method_exists($data, 'toArray')) {
@@ -469,11 +474,6 @@ private function normalizeValue(mixed $data): mixed
469474
'__class' => $data::class,
470475
'__datetime' => $data->format(DATE_RFC3339_EXTENDED),
471476
];
472-
} elseif ($data instanceof UnitEnum) {
473-
return [
474-
'__class' => $data::class,
475-
'__enum' => $data instanceof BackedEnum ? $data->value : $data->name,
476-
];
477477
} else {
478478
$objectData = get_object_vars($data);
479479

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace Tests\Support\Entity;
15+
16+
use ArrayObject;
17+
18+
/**
19+
* @extends ArrayObject<string, string>
20+
*/
21+
final class ArrayObjectWithToArray extends ArrayObject
22+
{
23+
/**
24+
* @return array<string, string>
25+
*/
26+
public function toArray(): array
27+
{
28+
return ['array' => 'same'];
29+
}
30+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace Tests\Support\Enum;
15+
16+
use JsonSerializable;
17+
18+
enum JsonSerializableStateUnitEnum implements JsonSerializable
19+
{
20+
case DRAFT;
21+
case PUBLISHED;
22+
23+
public function jsonSerialize(): mixed
24+
{
25+
return ['json' => $this->name];
26+
}
27+
}

tests/_support/Enum/StateEnum.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace Tests\Support\Enum;
15+
16+
enum StateEnum: string
17+
{
18+
case DRAFT = 'draft';
19+
case PUBLISHED = 'published';
20+
21+
public function toArray(): array
22+
{
23+
return self::cases();
24+
}
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace Tests\Support\Enum;
15+
16+
enum StateUnitEnum
17+
{
18+
case DRAFT;
19+
case PUBLISHED;
20+
21+
public function toArray(): array
22+
{
23+
return self::cases();
24+
}
25+
}

tests/system/Entity/EntityTest.php

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@
3030
use PHPUnit\Framework\Attributes\Group;
3131
use ReflectionException;
3232
use stdClass;
33+
use Tests\Support\Entity\ArrayObjectWithToArray;
3334
use Tests\Support\Entity\Cast\CastBase64;
3435
use Tests\Support\Entity\Cast\CastPassParameters;
3536
use Tests\Support\Entity\Cast\NotExtendsBaseCast;
3637
use Tests\Support\Enum\ColorEnum;
38+
use Tests\Support\Enum\JsonSerializableStateUnitEnum;
3739
use Tests\Support\Enum\RoleEnum;
40+
use Tests\Support\Enum\StateEnum;
41+
use Tests\Support\Enum\StateUnitEnum;
3842
use Tests\Support\Enum\StatusEnum;
3943
use Tests\Support\SomeEntity;
4044

@@ -1045,6 +1049,45 @@ public function testCastEnumSetWithUnitEnumObject(): void
10451049
$this->assertSame(ColorEnum::RED, $entity->color);
10461050
}
10471051

1052+
/**
1053+
* @see https://github.com/codeigniter4/CodeIgniter4/issues/10136
1054+
*/
1055+
public function testInjectRawDataWithBackedEnumThatHasToArrayMethod(): void
1056+
{
1057+
$entity = new class () extends Entity {};
1058+
1059+
$entity->injectRawData(['state' => StateEnum::DRAFT]);
1060+
1061+
$this->assertSame(StateEnum::DRAFT, $entity->toRawArray()['state']);
1062+
$this->assertFalse($entity->hasChanged('state'));
1063+
}
1064+
1065+
/**
1066+
* @see https://github.com/codeigniter4/CodeIgniter4/issues/10136
1067+
*/
1068+
public function testInjectRawDataWithUnitEnumThatHasToArrayMethod(): void
1069+
{
1070+
$entity = new class () extends Entity {};
1071+
1072+
$entity->injectRawData(['state' => StateUnitEnum::DRAFT]);
1073+
1074+
$this->assertSame(StateUnitEnum::DRAFT, $entity->toRawArray()['state']);
1075+
$this->assertFalse($entity->hasChanged('state'));
1076+
}
1077+
1078+
/**
1079+
* @see https://github.com/codeigniter4/CodeIgniter4/issues/10136
1080+
*/
1081+
public function testInjectRawDataWithUnitEnumThatImplementsJsonSerializable(): void
1082+
{
1083+
$entity = new class () extends Entity {};
1084+
1085+
$entity->injectRawData(['state' => JsonSerializableStateUnitEnum::DRAFT]);
1086+
1087+
$this->assertSame(JsonSerializableStateUnitEnum::DRAFT, $entity->toRawArray()['state']);
1088+
$this->assertFalse($entity->hasChanged('state'));
1089+
}
1090+
10481091
public function testAsArray(): void
10491092
{
10501093
$entity = $this->getEntity();
@@ -1975,6 +2018,46 @@ public function jsonSerialize(): mixed
19752018
$this->assertTrue($entity->hasChanged('data'));
19762019
}
19772020

2021+
public function testHasChangedPrefersJsonSerializableOverToArray(): void
2022+
{
2023+
$entity = new class () extends Entity {
2024+
protected $attributes = [
2025+
'data' => null,
2026+
];
2027+
};
2028+
2029+
$data = new class ('original') implements JsonSerializable {
2030+
public function __construct(private string $value)
2031+
{
2032+
}
2033+
2034+
public function jsonSerialize(): mixed
2035+
{
2036+
return ['json' => $this->value];
2037+
}
2038+
2039+
public function setValue(string $value): void
2040+
{
2041+
$this->value = $value;
2042+
}
2043+
2044+
/**
2045+
* @return array<string, string>
2046+
*/
2047+
public function toArray(): array
2048+
{
2049+
return ['array' => 'same'];
2050+
}
2051+
};
2052+
2053+
$entity->data = $data;
2054+
$entity->syncOriginal();
2055+
2056+
$data->setValue('modified');
2057+
2058+
$this->assertTrue($entity->hasChanged('data'));
2059+
}
2060+
19782061
public function testHasChangedDoesNotDetectUnchangedObject(): void
19792062
{
19802063
$entity = new class () extends Entity {
@@ -2278,6 +2361,50 @@ public function toArray(): array
22782361
$this->assertTrue($entity->hasChanged('custom'));
22792362
}
22802363

2364+
public function testHasChangedPrefersToArrayOverTraversable(): void
2365+
{
2366+
$entity = new class () extends Entity {
2367+
protected $attributes = [
2368+
'items' => null,
2369+
];
2370+
};
2371+
2372+
$items = new ArrayObjectWithToArray(['iterator' => 'original']);
2373+
2374+
$entity->items = $items;
2375+
$entity->syncOriginal();
2376+
2377+
$items->exchangeArray(['iterator' => 'modified']);
2378+
2379+
$this->assertFalse($entity->hasChanged('items'));
2380+
}
2381+
2382+
public function testHasChangedPrefersToArrayOverDateTimeInterface(): void
2383+
{
2384+
$entity = new class () extends Entity {
2385+
protected $attributes = [
2386+
'date' => null,
2387+
];
2388+
};
2389+
2390+
$date = new class ('2024-01-01 00:00:00') extends DateTime {
2391+
/**
2392+
* @return array<string, string>
2393+
*/
2394+
public function toArray(): array
2395+
{
2396+
return ['date' => 'same'];
2397+
}
2398+
};
2399+
2400+
$entity->date = $date;
2401+
$entity->syncOriginal();
2402+
2403+
$date->modify('+1 day');
2404+
2405+
$this->assertFalse($entity->hasChanged('date'));
2406+
}
2407+
22812408
public function testHasChangedScalarOptimizationWithNullValues(): void
22822409
{
22832410
$entity = new class () extends Entity {

user_guide_src/source/changelogs/v4.7.3.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Bugs Fixed
4545
- **Database:** Fixed a bug where ``BaseConnection::listTables()`` could return a sparse array when using cached table names after a table was dropped.
4646
- **Database:** Fixed a bug where the PostgreSQL driver's ``increment()`` and ``decrement()`` methods were not working for numeric columns.
4747
- **Database:** Fixed a bug where the SQLSRV driver's decrement method was adding instead of subtracting the decrement value when ``$castTextToInt`` was false.
48+
- **Entity:** Fixed a bug where ``Entity::normalizeValue()`` did not handle ``UnitEnum`` before checking for ``toArray()``, causing enums implementing ``toArray()`` to be incorrectly normalized as generic objects instead of enums.
4849
- **Kint:** Fixed a bug where stale Content Security Policy nonces were reused in worker mode, causing browser CSP violations for Debug Toolbar assets.
4950
- **Language:** Fixed a bug where ``Language::getLine()`` returned the literal dot-notation key instead of the nested array value when the requested key resolved to an intermediate array three or more levels deep.
5051
- **Toolbar:** Fixed a bug where the Logs collector raised an undefined property error when using a third-party PSR-3 logger.

0 commit comments

Comments
 (0)