diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 43182cfb9b2a..e3733bd0fc02 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -458,6 +458,11 @@ private function normalizeValue(mixed $data): mixed // Check for Entity instance (use raw values, recursive) if ($data instanceof self) { $objectData = $data->toRawArray(false, true); + } elseif ($data instanceof UnitEnum) { + return [ + '__class' => $data::class, + '__enum' => $data instanceof BackedEnum ? $data->value : $data->name, + ]; } elseif ($data instanceof JsonSerializable) { $objectData = $data->jsonSerialize(); } elseif (method_exists($data, 'toArray')) { @@ -469,11 +474,6 @@ private function normalizeValue(mixed $data): mixed '__class' => $data::class, '__datetime' => $data->format(DATE_RFC3339_EXTENDED), ]; - } elseif ($data instanceof UnitEnum) { - return [ - '__class' => $data::class, - '__enum' => $data instanceof BackedEnum ? $data->value : $data->name, - ]; } else { $objectData = get_object_vars($data); diff --git a/tests/_support/Entity/ArrayObjectWithToArray.php b/tests/_support/Entity/ArrayObjectWithToArray.php new file mode 100644 index 000000000000..2821cab1d0f9 --- /dev/null +++ b/tests/_support/Entity/ArrayObjectWithToArray.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Entity; + +use ArrayObject; + +/** + * @extends ArrayObject + */ +final class ArrayObjectWithToArray extends ArrayObject +{ + /** + * @return array + */ + public function toArray(): array + { + return ['array' => 'same']; + } +} diff --git a/tests/_support/Enum/JsonSerializableStateUnitEnum.php b/tests/_support/Enum/JsonSerializableStateUnitEnum.php new file mode 100644 index 000000000000..12f7ed9721ea --- /dev/null +++ b/tests/_support/Enum/JsonSerializableStateUnitEnum.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Enum; + +use JsonSerializable; + +enum JsonSerializableStateUnitEnum implements JsonSerializable +{ + case DRAFT; + case PUBLISHED; + + public function jsonSerialize(): mixed + { + return ['json' => $this->name]; + } +} diff --git a/tests/_support/Enum/StateEnum.php b/tests/_support/Enum/StateEnum.php new file mode 100644 index 000000000000..5b151e167862 --- /dev/null +++ b/tests/_support/Enum/StateEnum.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Enum; + +enum StateEnum: string +{ + case DRAFT = 'draft'; + case PUBLISHED = 'published'; + + public function toArray(): array + { + return self::cases(); + } +} diff --git a/tests/_support/Enum/StateUnitEnum.php b/tests/_support/Enum/StateUnitEnum.php new file mode 100644 index 000000000000..89626b05908b --- /dev/null +++ b/tests/_support/Enum/StateUnitEnum.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\Enum; + +enum StateUnitEnum +{ + case DRAFT; + case PUBLISHED; + + public function toArray(): array + { + return self::cases(); + } +} diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 7c19d9d09b89..52d1de2f5a8b 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -30,11 +30,15 @@ use PHPUnit\Framework\Attributes\Group; use ReflectionException; use stdClass; +use Tests\Support\Entity\ArrayObjectWithToArray; use Tests\Support\Entity\Cast\CastBase64; use Tests\Support\Entity\Cast\CastPassParameters; use Tests\Support\Entity\Cast\NotExtendsBaseCast; use Tests\Support\Enum\ColorEnum; +use Tests\Support\Enum\JsonSerializableStateUnitEnum; use Tests\Support\Enum\RoleEnum; +use Tests\Support\Enum\StateEnum; +use Tests\Support\Enum\StateUnitEnum; use Tests\Support\Enum\StatusEnum; use Tests\Support\SomeEntity; @@ -1045,6 +1049,45 @@ public function testCastEnumSetWithUnitEnumObject(): void $this->assertSame(ColorEnum::RED, $entity->color); } + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/10136 + */ + public function testInjectRawDataWithBackedEnumThatHasToArrayMethod(): void + { + $entity = new class () extends Entity {}; + + $entity->injectRawData(['state' => StateEnum::DRAFT]); + + $this->assertSame(StateEnum::DRAFT, $entity->toRawArray()['state']); + $this->assertFalse($entity->hasChanged('state')); + } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/10136 + */ + public function testInjectRawDataWithUnitEnumThatHasToArrayMethod(): void + { + $entity = new class () extends Entity {}; + + $entity->injectRawData(['state' => StateUnitEnum::DRAFT]); + + $this->assertSame(StateUnitEnum::DRAFT, $entity->toRawArray()['state']); + $this->assertFalse($entity->hasChanged('state')); + } + + /** + * @see https://github.com/codeigniter4/CodeIgniter4/issues/10136 + */ + public function testInjectRawDataWithUnitEnumThatImplementsJsonSerializable(): void + { + $entity = new class () extends Entity {}; + + $entity->injectRawData(['state' => JsonSerializableStateUnitEnum::DRAFT]); + + $this->assertSame(JsonSerializableStateUnitEnum::DRAFT, $entity->toRawArray()['state']); + $this->assertFalse($entity->hasChanged('state')); + } + public function testAsArray(): void { $entity = $this->getEntity(); @@ -1975,6 +2018,46 @@ public function jsonSerialize(): mixed $this->assertTrue($entity->hasChanged('data')); } + public function testHasChangedPrefersJsonSerializableOverToArray(): void + { + $entity = new class () extends Entity { + protected $attributes = [ + 'data' => null, + ]; + }; + + $data = new class ('original') implements JsonSerializable { + public function __construct(private string $value) + { + } + + public function jsonSerialize(): mixed + { + return ['json' => $this->value]; + } + + public function setValue(string $value): void + { + $this->value = $value; + } + + /** + * @return array + */ + public function toArray(): array + { + return ['array' => 'same']; + } + }; + + $entity->data = $data; + $entity->syncOriginal(); + + $data->setValue('modified'); + + $this->assertTrue($entity->hasChanged('data')); + } + public function testHasChangedDoesNotDetectUnchangedObject(): void { $entity = new class () extends Entity { @@ -2278,6 +2361,50 @@ public function toArray(): array $this->assertTrue($entity->hasChanged('custom')); } + public function testHasChangedPrefersToArrayOverTraversable(): void + { + $entity = new class () extends Entity { + protected $attributes = [ + 'items' => null, + ]; + }; + + $items = new ArrayObjectWithToArray(['iterator' => 'original']); + + $entity->items = $items; + $entity->syncOriginal(); + + $items->exchangeArray(['iterator' => 'modified']); + + $this->assertFalse($entity->hasChanged('items')); + } + + public function testHasChangedPrefersToArrayOverDateTimeInterface(): void + { + $entity = new class () extends Entity { + protected $attributes = [ + 'date' => null, + ]; + }; + + $date = new class ('2024-01-01 00:00:00') extends DateTime { + /** + * @return array + */ + public function toArray(): array + { + return ['date' => 'same']; + } + }; + + $entity->date = $date; + $entity->syncOriginal(); + + $date->modify('+1 day'); + + $this->assertFalse($entity->hasChanged('date')); + } + public function testHasChangedScalarOptimizationWithNullValues(): void { $entity = new class () extends Entity { diff --git a/user_guide_src/source/changelogs/v4.7.3.rst b/user_guide_src/source/changelogs/v4.7.3.rst index dbc930e12304..d506845dd31b 100644 --- a/user_guide_src/source/changelogs/v4.7.3.rst +++ b/user_guide_src/source/changelogs/v4.7.3.rst @@ -45,6 +45,7 @@ Bugs Fixed - **Database:** Fixed a bug where ``BaseConnection::listTables()`` could return a sparse array when using cached table names after a table was dropped. - **Database:** Fixed a bug where the PostgreSQL driver's ``increment()`` and ``decrement()`` methods were not working for numeric columns. - **Database:** Fixed a bug where the SQLSRV driver's decrement method was adding instead of subtracting the decrement value when ``$castTextToInt`` was false. +- **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. - **Kint:** Fixed a bug where stale Content Security Policy nonces were reused in worker mode, causing browser CSP violations for Debug Toolbar assets. - **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. - **Toolbar:** Fixed a bug where the Logs collector raised an undefined property error when using a third-party PSR-3 logger.