Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion system/DataCaster/Cast/DatetimeCast.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\I18n\Time;
use DateTimeInterface;
use Exception;

/**
* Class DatetimeCast
Expand Down Expand Up @@ -51,7 +53,15 @@ public static function set(
array $params = [],
?object $helper = null,
): string {
if (! $value instanceof Time) {
if (is_string($value)) {
try {
$value = Time::parse($value);
} catch (Exception) {
self::invalidTypeValueError($value);
}
}

if (! $value instanceof DateTimeInterface) {
self::invalidTypeValueError($value);
}

Expand Down
123 changes: 66 additions & 57 deletions system/Entity/Entity.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,84 +261,93 @@ public function toRawArray(bool $onlyChanged = false, bool $recursive = false):

// When returning everything
if (! $onlyChanged) {
return $recursive
? array_map($convert, $this->attributes)
: $this->attributes;
}

// When filtering by changed values only
$return = [];

foreach ($this->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)) {
$originalValue = $this->original[$key] ?? null;
$result = array_map($convert, $this->attributes);
} else {
// When filtering by changed values only
$result = [];

foreach ($this->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)) {
$originalValue = $this->original[$key] ?? null;

if (! is_string($originalValue)) {
// No original or invalid format, export all entities
$converted = [];

foreach ($value as $idx => $item) {
$converted[$idx] = $item->toRawArray(false, true);
}
$result[$key] = $converted;

continue;
}

if (! is_string($originalValue)) {
// No original or invalid format, export all entities
$converted = [];
// Decode original array structure for per-entity comparison
$originalArray = json_decode($originalValue, true);
$converted = [];

foreach ($value as $idx => $item) {
$converted[$idx] = $item->toRawArray(false, true);
// Compare current entity against its original state
$currentNormalized = $this->normalizeValue($item);
$originalNormalized = $originalArray[$idx] ?? null;

// Only include if changed, new, or can't determine
if ($originalNormalized === null || $currentNormalized !== $originalNormalized) {
$converted[$idx] = $item->toRawArray(false, true);
}
}
$return[$key] = $converted;

continue;
}

// Decode original array structure for per-entity comparison
$originalArray = json_decode($originalValue, true);
$converted = [];

foreach ($value as $idx => $item) {
// Compare current entity against its original state
$currentNormalized = $this->normalizeValue($item);
$originalNormalized = $originalArray[$idx] ?? null;

// Only include if changed, new, or can't determine
if ($originalNormalized === null || $currentNormalized !== $originalNormalized) {
$converted[$idx] = $item->toRawArray(false, true);
// Only include this property if at least one entity changed
if ($converted !== []) {
$result[$key] = $converted;
}
}

// Only include this property if at least one entity changed
if ($converted !== []) {
$return[$key] = $converted;
continue;
}

continue;
}
// For all other cases, use hasChanged()
if (! $this->hasChanged($key)) {
continue;
}

// For all other cases, use hasChanged()
if (! $this->hasChanged($key)) {
continue;
}
if ($recursive) {
// Special handling for arrays (mixed or not all entities)
if (is_array($value)) {
$converted = [];

if ($recursive) {
// Special handling for arrays (mixed or not all entities)
if (is_array($value)) {
$converted = [];
foreach ($value as $idx => $item) {
$converted[$idx] = $item instanceof self ? $item->toRawArray(false, true) : $convert($item);
}
$result[$key] = $converted;

foreach ($value as $idx => $item) {
$converted[$idx] = $item instanceof self ? $item->toRawArray(false, true) : $convert($item);
continue;
}
$return[$key] = $converted;

// default recursive conversion
$result[$key] = $convert($value);

continue;
}

// default recursive conversion
$return[$key] = $convert($value);

continue;
// non-recursive changed value
$result[$key] = $convert($value);
}
}

// Convert DateTime objects to string for user-facing calls
if (! $recursive) {
$result = array_map(static function ($value) {
if ($value instanceof DateTimeInterface) {
return method_exists($value, '__toString') ? (string) $value : $value->format('Y-m-d H:i:s');
}

// non-recursive changed value
$return[$key] = $value;
return $value;
}, $result);
}

return $return;
return $result;
}

/**
Expand Down
122 changes: 122 additions & 0 deletions tests/system/Entity/EntityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,41 @@ public function testDateMutationTimeToTime(): void
$this->assertCloseEnoughString($dt->format('Y-m-d H:i:s'), $time->format('Y-m-d H:i:s'));
}

public function testToRawArrayConvertsDateTimeToString(): void
{
$entity = new class () extends Entity {
protected $attributes = [
'created_at' => null,
'updated_at' => null,
];
protected $original = [
'created_at' => null,
'updated_at' => null,
];
};

$entity->created_at = '2023-12-12 12:12:12';
$entity->updated_at = '2023-12-13 13:13:13';

$raw = $entity->toRawArray();

// toRawArray() should return primitive types, not objects
$this->assertIsString($raw['created_at']);
$this->assertSame('2023-12-12 12:12:12', $raw['created_at']);
$this->assertIsString($raw['updated_at']);
$this->assertSame('2023-12-13 13:13:13', $raw['updated_at']);

// Attributes themselves should still contain Time objects
$attrs = $this->getPrivateProperty($entity, 'attributes');
$this->assertInstanceOf(Time::class, $attrs['created_at']);
$this->assertInstanceOf(Time::class, $attrs['updated_at']);

// toArray() should still return Time objects (no regression)
$array = $entity->toArray();
$this->assertInstanceOf(Time::class, $array['created_at']);
$this->assertInstanceOf(Time::class, $array['updated_at']);
}

public function testCastInteger(): void
{
$entity = $this->getCastEntity();
Expand Down Expand Up @@ -1421,6 +1456,93 @@ public function testToRawArrayOnlyChanged(): void
], $result);
}

public function testToRawArrayConvertsDateTimeToStringOnlyWhenNonRecursive(): void
{
$entity = $this->getEntity();

// Non-recursive: DateTime becomes string
$entity->created_at = '2024-03-15 10:30:00';
$raw = $entity->toRawArray();
$this->assertIsString($raw['created_at']);

// Recursive: DateTime stays Time
$recursive = $entity->toRawArray(false, true);
$this->assertInstanceOf(Time::class, $recursive['created_at']);

// Non-recursive onlyChanged: also converts to string
$entity->syncOriginal();
$entity->created_at = '2024-06-15 14:30:00';
$changed = $entity->toRawArray(true);
$this->assertIsString($changed['created_at']);

// Recursive onlyChanged: preserves Time
$changedRecursive = $entity->toRawArray(true, true);
$this->assertInstanceOf(Time::class, $changedRecursive['created_at']);

// Null values stay null regardless of mode
$entity2 = $this->getEntity();
$this->assertNull($entity2->toRawArray()['created_at']);
$this->assertNull($entity2->toRawArray(false, true)['created_at']);

// toArray() should not be affected (still returns Time)
$arr = $entity->toArray();
$this->assertInstanceOf(Time::class, $arr['createdAt']);
}

public function testToRawArrayRecursivePreservesTimeObjectsInNestedEntities(): void
{
$child = $this->getEntity();
$child->created_at = '2024-03-10 08:00:00';

$parent = $this->getEntity();
$parent->created_at = '2024-03-15 10:30:00';
$parent->entity = $child;

$result = $parent->toRawArray(false, true);

$this->assertInstanceOf(Time::class, $result['created_at']);
$this->assertIsArray($result['entity']);
$this->assertInstanceOf(Time::class, $result['entity']['created_at']);

// Non-recursive: converts to string
$nonRecursive = $parent->toRawArray();
$this->assertIsString($nonRecursive['created_at']);

// toArray() uses datamapped keys
$arr = $parent->toArray();
$this->assertInstanceOf(Time::class, $arr['createdAt']);
}

public function testToRawArrayRecursiveWithMixedAttributes(): void
{
$entity = new class () extends Entity {
protected $attributes = [
'name' => null,
'created_at' => null,
'count' => null,
];
protected $original = [
'name' => null,
'created_at' => null,
'count' => null,
];
};

$entity->name = 'test';
$entity->count = 42;
$entity->created_at = '2024-08-20 16:45:00';

// Recursive: scalars unchanged, DateTime preserved as Time
$result = $entity->toRawArray(false, true);
$this->assertSame('test', $result['name']);
$this->assertSame(42, $result['count']);
$this->assertInstanceOf(Time::class, $result['created_at']);

// Non-recursive: DateTime converted to string
$result2 = $entity->toRawArray();
$this->assertIsString($result2['created_at']);
}

public function testFilledConstruction(): void
{
$data = [
Expand Down
2 changes: 2 additions & 0 deletions user_guide_src/source/changelogs/v4.7.5.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Deprecations
Bugs Fixed
**********

- **Entity:** Fixed a bug where ``toRawArray()`` returned ``Time`` objects instead of ISO-8601 strings for date fields.

- **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors.

See the repo's
Expand Down
2 changes: 1 addition & 1 deletion utils/phpstan-baseline/empty.notAllowed.neon
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# total 212 errors
# total 205 errors

parameters:
ignoreErrors:
Expand Down
2 changes: 1 addition & 1 deletion utils/phpstan-baseline/loader.neon
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# total 1819 errors
# total 1812 errors

includes:
- argument.type.neon
Expand Down
Loading