diff --git a/docs/changelogs/v4.7.7.md b/docs/changelogs/v4.7.7.md index cdf4eb2db148..7b60787751e2 100644 --- a/docs/changelogs/v4.7.7.md +++ b/docs/changelogs/v4.7.7.md @@ -14,6 +14,7 @@ Release Date: Unreleased ## Bugs Fixed +- **Entity:** Fixed a bug where `Entity::toRawArray()` returned a `Time` instance for a date attribute that had been assigned a string value, while the same attribute was returned as a string when the entity was populated from the database. `toRawArray()` now returns the string representation of date attributes in both cases. - **Security:** Fixed a bug where `Security::__construct()` threw a `TypeError` when the CSRF cookie was an array, causing an HTTP 500 response for affected requests. - **Worker Mode:** Fixed a bug where every request that ended with an uncaught exception, including a `404 Page Not Found` without a 404 override, called `exit()` in the exception handler. This ended the worker script, so FrankenPHP booted a new one, and the worker loop skipped its cleanup after the request. The exception handler now returns to the worker loop. diff --git a/system/Entity/Entity.php b/system/Entity/Entity.php index 30c9e37fa1a8..c3c093f42ac2 100644 --- a/system/Entity/Entity.php +++ b/system/Entity/Entity.php @@ -230,6 +230,20 @@ public function toArray(bool $onlyChanged = false, bool $cast = true, bool $recu */ public function toRawArray(bool $onlyChanged = false, bool $recursive = false): array { + // Date attributes that were assigned at runtime are mutated to + // Time instances when they are set (see __set()), while the same + // attributes hold plain strings when the entity is populated + // from the database (see injectRawData()). The raw value of a + // date attribute is its string representation, so normalize any + // Time instances back to strings to keep the output consistent. + $attributes = $this->attributes; + + foreach ($attributes as $key => $value) { + if ($value instanceof Time && in_array($key, $this->dates, true)) { + $attributes[$key] = (string) $value; + } + } + $convert = static function ($value) use (&$convert, $recursive) { if (! $recursive) { return $value; @@ -260,14 +274,14 @@ public function toRawArray(bool $onlyChanged = false, bool $recursive = false): // When returning everything if (! $onlyChanged) { return $recursive - ? array_map($convert, $this->attributes) - : $this->attributes; + ? array_map($convert, $attributes) + : $attributes; } // When filtering by changed values only $return = []; - foreach ($this->attributes as $key => $value) { + foreach ($attributes as $key => $value) { // Special handling for arrays of entities in recursive mode // Skip hasChanged() and do per-entity comparison directly if ($recursive && is_array($value) && $this->containsOnlyEntities($value)) { diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 3aff3fac3a10..57c8e10d335e 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -80,6 +80,29 @@ public function testSetArrayToPropertyNamedAttributes(): void $this->assertSame($expected, $entity->toRawArray()); } + public function testToRawArrayReturnsStringForAssignedDateAttribute(): void + { + $entity = new class () extends Entity { + protected $dates = ['created_at', 'updated_at', 'deleted_at']; + }; + + // Simulate an entity fetched from the database. + $entity->injectRawData([ + 'created_at' => '2023-12-07 04:00:48', + 'updated_at' => '2023-12-07 04:00:48', + 'deleted_at' => null, + ]); + + $entity->updated_at = '2023-12-12 12:12:12'; + + // The raw value must stay the assigned string, not a Time instance. + $this->assertSame('2023-12-12 12:12:12', $entity->toRawArray()['updated_at']); + + // Retrieval still mutates the value into a Time instance. + $this->assertInstanceOf(Time::class, $entity->updated_at); + $this->assertSame('2023-12-12 12:12:12', $entity->updated_at->format('Y-m-d H:i:s')); + } + public function testSetGetAttributesMethod(): void { $entity = new class () extends Entity {