Skip to content

fix: convert DateTimeInterface objects to strings in Entity::toRawArray#10402

Open
gr8man wants to merge 1 commit into
codeigniter4:developfrom
gr8man:fix/8302-toRawArray-datetime
Open

fix: convert DateTimeInterface objects to strings in Entity::toRawArray#10402
gr8man wants to merge 1 commit into
codeigniter4:developfrom
gr8man:fix/8302-toRawArray-datetime

Conversation

@gr8man

@gr8man gr8man commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Description
Supersedes #10207. Fixes #8302.

Entity::toRawArray() was returning Time objects instead of primitive strings for date fields. This PR fixes it by converting all DateTimeInterface objects to strings in non-recursive mode.

Unlike the original PR (#10207), this version safely handles native PHP DateTime objects to prevent Fatal Errors (Object of class DateTime could not be converted to string), by falling back to $value->format('Y-m-d H:i:s') if __toString() is not implemented.

The conversion only applies to non-recursive calls ($recursive = false). The recursive mode preserves Time objects, which are then handled by BaseModel::timeToString() in the non-cast path and by DataConverter::toDataSource() in the cast path — so model saving/casting is unaffected.

Changes:

  • Safely stringify DateTimeInterface objects in Entity::toRawArray() (non-recursive only)
  • Updated DatetimeCast::set() to accept strings and any DateTimeInterface for round-trip compatibility
  • Added test cases: basic conversion, non-recursive vs recursive, nested entities, mixed attributes

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • [] Conforms to style guide

@gr8man gr8man changed the title Fix/8302 to raw array datetime fix: convert DateTimeInterface objects to strings in Entity::toRawArray Jul 10, 2026
@gr8man gr8man force-pushed the fix/8302-toRawArray-datetime branch 4 times, most recently from 2346fdc to d87f3a6 Compare July 10, 2026 21:08
@gr8man gr8man force-pushed the fix/8302-toRawArray-datetime branch from 4c3f524 to 3eceb0c Compare July 10, 2026 21:10

@michalsn michalsn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change affects all date-time attributes, not only Entity date fields, and will likely break model casting.

@neznaika0

Copy link
Copy Markdown
Contributor

I agree, I do not know how to describe it.
What kind of result should we get? After all, DataConveter/DataCaster does not prohibit having a Time object in its raw form. Hard conversion to a string is incorrect.

@gr8man

gr8man commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for the review @michalsn. I've traced through all code paths where toRawArray() is consumed:

  • Non-cast save path (BaseModel::objectToArray): calls toRawArray() with $recursive = true, which skips the DateTime conversion — timeToString() handles it afterward. No change in behavior ✅
  • Cast save path (DataConverter::extract): toRawArray() output goes through toDataSource(), which applies the proper 'set' cast on every field. The updated DatetimeCast::set() already accepts strings, so the round-trip is safe ✅
  • Direct user call ($entity->toRawArray()): this is the actual fix for Bug: Entity::toRawArray() may return Time object #8302 — now returns strings instead of Time objects ✅

Model casting is not affected because the conversion layer (toDataSource / timeToString) sits between toRawArray() and the database in both paths. The change only affects user-facing non-recursive calls, which is exactly what the issue reports.

@michalsn michalsn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said, it breaks model casting.

public function testExtractEntityWithTimestampCast(): void
{
    $converter = $this->createDataConverter([
        'updated_at' => 'timestamp',
    ]);

    $entity = new class extends Entity {
        protected $dates = [];
    };
    $entity->injectRawData([
        'updated_at' => Time::createFromTimestamp(1_784_092_299),
    ]);

    $data = $converter->extract($entity);

    $this->assertSame(1_784_092_299, $data['updated_at']);
}

public function testExtractEntityPreservesMicrosecondsWithDatetimeCast(): void
{
    $converter = $this->createDataConverter(
        ['updated_at' => 'datetime[us]'],
        [],
        db_connect(),
    );

    $entity = new class extends Entity {
        protected $dates = [];
    };
    $entity->injectRawData([
        'updated_at' => Time::createFromFormat('Y-m-d H:i:s.u', '2026-07-15 00:00:01.123456'),
    ]);

    $data = $converter->extract($entity);

    $this->assertSame('2026-07-15 00:00:01.123456', $data['updated_at']);
}

I don't believe it can be fixed safely in v4 without introducing BC breaks. It may need to be addressed as part of the Entity redesign in v5.

Comment on lines -54 to +64
if (! $value instanceof Time) {
if (is_string($value)) {
try {
$value = Time::parse($value);
} catch (Exception) {
self::invalidTypeValueError($value);
}
}

if (! $value instanceof DateTimeInterface) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now relative values such as "tomorrow" are accepted, which broadens the caster's behavior beyond this fix.

Comment thread system/Entity/Entity.php
Comment on lines +339 to +344
// 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');
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now every top-level DateTimeInterface value is converted to a string, regardless of whether the field is declared in Entity $dates or $casts.

Bugs Fixed
**********

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even now, we don't return ISO-8601 strings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Entity::toRawArray() may return Time object

3 participants